RAHHH
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.internet}.
|
||||
"""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,169 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
POSIX implementation of local network interface enumeration.
|
||||
"""
|
||||
|
||||
|
||||
import socket
|
||||
import sys
|
||||
from ctypes import (
|
||||
CDLL,
|
||||
POINTER,
|
||||
Structure,
|
||||
c_char_p,
|
||||
c_int,
|
||||
c_ubyte,
|
||||
c_uint8,
|
||||
c_uint32,
|
||||
c_ushort,
|
||||
c_void_p,
|
||||
cast,
|
||||
pointer,
|
||||
)
|
||||
from ctypes.util import find_library
|
||||
from socket import AF_INET, AF_INET6, inet_ntop
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
from twisted.python.compat import nativeString
|
||||
|
||||
libc = CDLL(find_library("c") or "")
|
||||
|
||||
if sys.platform.startswith("freebsd") or sys.platform == "darwin":
|
||||
_sockaddrCommon: List[Tuple[str, Any]] = [
|
||||
("sin_len", c_uint8),
|
||||
("sin_family", c_uint8),
|
||||
]
|
||||
else:
|
||||
_sockaddrCommon: List[Tuple[str, Any]] = [
|
||||
("sin_family", c_ushort),
|
||||
]
|
||||
|
||||
|
||||
class in_addr(Structure):
|
||||
_fields_ = [
|
||||
("in_addr", c_ubyte * 4),
|
||||
]
|
||||
|
||||
|
||||
class in6_addr(Structure):
|
||||
_fields_ = [
|
||||
("in_addr", c_ubyte * 16),
|
||||
]
|
||||
|
||||
|
||||
class sockaddr(Structure):
|
||||
_fields_ = _sockaddrCommon + [
|
||||
("sin_port", c_ushort),
|
||||
]
|
||||
|
||||
|
||||
class sockaddr_in(Structure):
|
||||
_fields_ = _sockaddrCommon + [
|
||||
("sin_port", c_ushort),
|
||||
("sin_addr", in_addr),
|
||||
]
|
||||
|
||||
|
||||
class sockaddr_in6(Structure):
|
||||
_fields_ = _sockaddrCommon + [
|
||||
("sin_port", c_ushort),
|
||||
("sin_flowinfo", c_uint32),
|
||||
("sin_addr", in6_addr),
|
||||
]
|
||||
|
||||
|
||||
class ifaddrs(Structure):
|
||||
pass
|
||||
|
||||
|
||||
ifaddrs_p = POINTER(ifaddrs)
|
||||
ifaddrs._fields_ = [
|
||||
("ifa_next", ifaddrs_p),
|
||||
("ifa_name", c_char_p),
|
||||
("ifa_flags", c_uint32),
|
||||
("ifa_addr", POINTER(sockaddr)),
|
||||
("ifa_netmask", POINTER(sockaddr)),
|
||||
("ifa_dstaddr", POINTER(sockaddr)),
|
||||
("ifa_data", c_void_p),
|
||||
]
|
||||
|
||||
getifaddrs = libc.getifaddrs
|
||||
getifaddrs.argtypes = [POINTER(ifaddrs_p)]
|
||||
getifaddrs.restype = c_int
|
||||
|
||||
freeifaddrs = libc.freeifaddrs
|
||||
freeifaddrs.argtypes = [ifaddrs_p]
|
||||
|
||||
|
||||
def _maybeCleanupScopeIndex(family, packed):
|
||||
"""
|
||||
On FreeBSD, kill the embedded interface indices in link-local scoped
|
||||
addresses.
|
||||
|
||||
@param family: The address family of the packed address - one of the
|
||||
I{socket.AF_*} constants.
|
||||
|
||||
@param packed: The packed representation of the address (ie, the bytes of a
|
||||
I{in_addr} field).
|
||||
@type packed: L{bytes}
|
||||
|
||||
@return: The packed address with any FreeBSD-specific extra bits cleared.
|
||||
@rtype: L{bytes}
|
||||
|
||||
@see: U{https://twistedmatrix.com/trac/ticket/6843}
|
||||
@see: U{http://www.freebsd.org/doc/en/books/developers-handbook/ipv6.html#ipv6-scope-index}
|
||||
|
||||
@note: Indications are that the need for this will be gone in FreeBSD >=10.
|
||||
"""
|
||||
if sys.platform.startswith("freebsd") and packed[:2] == b"\xfe\x80":
|
||||
return packed[:2] + b"\x00\x00" + packed[4:]
|
||||
return packed
|
||||
|
||||
|
||||
def _interfaces():
|
||||
"""
|
||||
Call C{getifaddrs(3)} and return a list of tuples of interface name, address
|
||||
family, and human-readable address representing its results.
|
||||
"""
|
||||
ifaddrs = ifaddrs_p()
|
||||
if getifaddrs(pointer(ifaddrs)) < 0:
|
||||
raise OSError()
|
||||
results = []
|
||||
try:
|
||||
while ifaddrs:
|
||||
if ifaddrs[0].ifa_addr:
|
||||
family = ifaddrs[0].ifa_addr[0].sin_family
|
||||
if family == AF_INET:
|
||||
addr = cast(ifaddrs[0].ifa_addr, POINTER(sockaddr_in))
|
||||
elif family == AF_INET6:
|
||||
addr = cast(ifaddrs[0].ifa_addr, POINTER(sockaddr_in6))
|
||||
else:
|
||||
addr = None
|
||||
|
||||
if addr:
|
||||
packed = bytes(addr[0].sin_addr.in_addr[:])
|
||||
packed = _maybeCleanupScopeIndex(family, packed)
|
||||
results.append(
|
||||
(ifaddrs[0].ifa_name, family, inet_ntop(family, packed))
|
||||
)
|
||||
|
||||
ifaddrs = ifaddrs[0].ifa_next
|
||||
finally:
|
||||
freeifaddrs(ifaddrs)
|
||||
return results
|
||||
|
||||
|
||||
def posixGetLinkLocalIPv6Addresses():
|
||||
"""
|
||||
Return a list of strings in colon-hex format representing all the link local
|
||||
IPv6 addresses available on the system, as reported by I{getifaddrs(3)}.
|
||||
"""
|
||||
retList = []
|
||||
for interface, family, address in _interfaces():
|
||||
interface = nativeString(interface)
|
||||
address = nativeString(address)
|
||||
if family == socket.AF_INET6 and address.startswith("fe80:"):
|
||||
retList.append(f"{address}%{interface}")
|
||||
return retList
|
||||
@@ -0,0 +1,137 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Windows implementation of local network interface enumeration.
|
||||
"""
|
||||
|
||||
from ctypes import ( # type: ignore[attr-defined]
|
||||
POINTER,
|
||||
Structure,
|
||||
WinDLL,
|
||||
byref,
|
||||
c_int,
|
||||
c_void_p,
|
||||
cast,
|
||||
create_string_buffer,
|
||||
create_unicode_buffer,
|
||||
wstring_at,
|
||||
)
|
||||
from socket import AF_INET6, SOCK_STREAM, socket
|
||||
|
||||
WS2_32 = WinDLL("ws2_32")
|
||||
|
||||
SOCKET = c_int
|
||||
DWORD = c_int
|
||||
LPVOID = c_void_p
|
||||
LPSOCKADDR = c_void_p
|
||||
LPWSAPROTOCOL_INFO = c_void_p
|
||||
LPTSTR = c_void_p
|
||||
LPDWORD = c_void_p
|
||||
LPWSAOVERLAPPED = c_void_p
|
||||
LPWSAOVERLAPPED_COMPLETION_ROUTINE = c_void_p
|
||||
|
||||
# http://msdn.microsoft.com/en-us/library/ms741621(v=VS.85).aspx
|
||||
# int WSAIoctl(
|
||||
# __in SOCKET s,
|
||||
# __in DWORD dwIoControlCode,
|
||||
# __in LPVOID lpvInBuffer,
|
||||
# __in DWORD cbInBuffer,
|
||||
# __out LPVOID lpvOutBuffer,
|
||||
# __in DWORD cbOutBuffer,
|
||||
# __out LPDWORD lpcbBytesReturned,
|
||||
# __in LPWSAOVERLAPPED lpOverlapped,
|
||||
# __in LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
|
||||
# );
|
||||
WSAIoctl = WS2_32.WSAIoctl
|
||||
WSAIoctl.argtypes = [
|
||||
SOCKET,
|
||||
DWORD,
|
||||
LPVOID,
|
||||
DWORD,
|
||||
LPVOID,
|
||||
DWORD,
|
||||
LPDWORD,
|
||||
LPWSAOVERLAPPED,
|
||||
LPWSAOVERLAPPED_COMPLETION_ROUTINE,
|
||||
]
|
||||
WSAIoctl.restype = c_int
|
||||
|
||||
# http://msdn.microsoft.com/en-us/library/ms741516(VS.85).aspx
|
||||
# INT WSAAPI WSAAddressToString(
|
||||
# __in LPSOCKADDR lpsaAddress,
|
||||
# __in DWORD dwAddressLength,
|
||||
# __in_opt LPWSAPROTOCOL_INFO lpProtocolInfo,
|
||||
# __inout LPTSTR lpszAddressString,
|
||||
# __inout LPDWORD lpdwAddressStringLength
|
||||
# );
|
||||
WSAAddressToString = WS2_32.WSAAddressToStringW
|
||||
WSAAddressToString.argtypes = [LPSOCKADDR, DWORD, LPWSAPROTOCOL_INFO, LPTSTR, LPDWORD]
|
||||
WSAAddressToString.restype = c_int
|
||||
|
||||
|
||||
SIO_ADDRESS_LIST_QUERY = 0x48000016
|
||||
WSAEFAULT = 10014
|
||||
|
||||
|
||||
class SOCKET_ADDRESS(Structure):
|
||||
_fields_ = [("lpSockaddr", c_void_p), ("iSockaddrLength", c_int)]
|
||||
|
||||
|
||||
def make_SAL(ln):
|
||||
class SOCKET_ADDRESS_LIST(Structure):
|
||||
_fields_ = [("iAddressCount", c_int), ("Address", SOCKET_ADDRESS * ln)]
|
||||
|
||||
return SOCKET_ADDRESS_LIST
|
||||
|
||||
|
||||
def win32GetLinkLocalIPv6Addresses():
|
||||
"""
|
||||
Return a list of strings in colon-hex format representing all the link local
|
||||
IPv6 addresses available on the system, as reported by
|
||||
I{WSAIoctl}/C{SIO_ADDRESS_LIST_QUERY}.
|
||||
"""
|
||||
s = socket(AF_INET6, SOCK_STREAM)
|
||||
size = 4096
|
||||
retBytes = c_int()
|
||||
for i in range(2):
|
||||
buf = create_string_buffer(size)
|
||||
ret = WSAIoctl(
|
||||
s.fileno(), SIO_ADDRESS_LIST_QUERY, 0, 0, buf, size, byref(retBytes), 0, 0
|
||||
)
|
||||
|
||||
# WSAIoctl might fail with WSAEFAULT, which means there was not enough
|
||||
# space in the buffer we gave it. There's no way to check the errno
|
||||
# until Python 2.6, so we don't even try. :/ Maybe if retBytes is still
|
||||
# 0 another error happened, though.
|
||||
if ret and retBytes.value:
|
||||
size = retBytes.value
|
||||
else:
|
||||
break
|
||||
|
||||
# If it failed, then we'll just have to give up. Still no way to see why.
|
||||
if ret:
|
||||
raise RuntimeError("WSAIoctl failure")
|
||||
|
||||
addrList = cast(buf, POINTER(make_SAL(0)))
|
||||
addrCount = addrList[0].iAddressCount
|
||||
addrList = cast(buf, POINTER(make_SAL(addrCount)))
|
||||
|
||||
addressStringBufLength = 1024
|
||||
addressStringBuf = create_unicode_buffer(addressStringBufLength)
|
||||
|
||||
retList = []
|
||||
for i in range(addrList[0].iAddressCount):
|
||||
retBytes.value = addressStringBufLength
|
||||
address = addrList[0].Address[i]
|
||||
ret = WSAAddressToString(
|
||||
address.lpSockaddr,
|
||||
address.iSockaddrLength,
|
||||
0,
|
||||
addressStringBuf,
|
||||
byref(retBytes),
|
||||
)
|
||||
if ret:
|
||||
raise RuntimeError("WSAAddressToString failure")
|
||||
retList.append(wstring_at(addressStringBuf))
|
||||
return [addr for addr in retList if "%" in addr]
|
||||
@@ -0,0 +1,594 @@
|
||||
# -*- test-case-name: twisted.internet.test.test_tcp -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Various helpers for tests for connection-oriented transports.
|
||||
"""
|
||||
|
||||
|
||||
import socket
|
||||
from gc import collect
|
||||
from typing import Optional
|
||||
from weakref import ref
|
||||
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.internet.defer import Deferred, gatherResults
|
||||
from twisted.internet.interfaces import IConnector, IReactorFDSet
|
||||
from twisted.internet.protocol import ClientFactory, Protocol, ServerFactory
|
||||
from twisted.internet.test.reactormixins import needsRunningReactor
|
||||
from twisted.python import context, log
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.log import ILogContext, err, msg
|
||||
from twisted.python.runtime import platform
|
||||
from twisted.test.test_tcp import ClosingProtocol
|
||||
from twisted.trial.unittest import SkipTest
|
||||
|
||||
|
||||
def findFreePort(interface="127.0.0.1", family=socket.AF_INET, type=socket.SOCK_STREAM):
|
||||
"""
|
||||
Ask the platform to allocate a free port on the specified interface, then
|
||||
release the socket and return the address which was allocated.
|
||||
|
||||
@param interface: The local address to try to bind the port on.
|
||||
@type interface: C{str}
|
||||
|
||||
@param type: The socket type which will use the resulting port.
|
||||
|
||||
@return: A two-tuple of address and port, like that returned by
|
||||
L{socket.getsockname}.
|
||||
"""
|
||||
addr = socket.getaddrinfo(interface, 0)[0][4]
|
||||
probe = socket.socket(family, type)
|
||||
try:
|
||||
probe.bind(addr)
|
||||
if family == socket.AF_INET6:
|
||||
sockname = probe.getsockname()
|
||||
hostname = socket.getnameinfo(
|
||||
sockname, socket.NI_NUMERICHOST | socket.NI_NUMERICSERV
|
||||
)[0]
|
||||
return (hostname, sockname[1])
|
||||
else:
|
||||
return probe.getsockname()
|
||||
finally:
|
||||
probe.close()
|
||||
|
||||
|
||||
class ConnectableProtocol(Protocol):
|
||||
"""
|
||||
A protocol to be used with L{runProtocolsWithReactor}.
|
||||
|
||||
The protocol and its pair should eventually disconnect from each other.
|
||||
|
||||
@ivar reactor: The reactor used in this test.
|
||||
|
||||
@ivar disconnectReason: The L{Failure} passed to C{connectionLost}.
|
||||
|
||||
@ivar _done: A L{Deferred} which will be fired when the connection is
|
||||
lost.
|
||||
"""
|
||||
|
||||
disconnectReason = None
|
||||
|
||||
def _setAttributes(self, reactor, done):
|
||||
"""
|
||||
Set attributes on the protocol that are known only externally; this
|
||||
will be called by L{runProtocolsWithReactor} when this protocol is
|
||||
instantiated.
|
||||
|
||||
@param reactor: The reactor used in this test.
|
||||
|
||||
@param done: A L{Deferred} which will be fired when the connection is
|
||||
lost.
|
||||
"""
|
||||
self.reactor = reactor
|
||||
self._done = done
|
||||
|
||||
def connectionLost(self, reason):
|
||||
self.disconnectReason = reason
|
||||
self._done.callback(None)
|
||||
del self._done
|
||||
|
||||
|
||||
class EndpointCreator:
|
||||
"""
|
||||
Create client and server endpoints that know how to connect to each other.
|
||||
"""
|
||||
|
||||
def server(self, reactor):
|
||||
"""
|
||||
Return an object providing C{IStreamServerEndpoint} for use in creating
|
||||
a server to use to establish the connection type to be tested.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def client(self, reactor, serverAddress):
|
||||
"""
|
||||
Return an object providing C{IStreamClientEndpoint} for use in creating
|
||||
a client to use to establish the connection type to be tested.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class _SingleProtocolFactory(ClientFactory):
|
||||
"""
|
||||
Factory to be used by L{runProtocolsWithReactor}.
|
||||
|
||||
It always returns the same protocol (i.e. is intended for only a single
|
||||
connection).
|
||||
"""
|
||||
|
||||
def __init__(self, protocol):
|
||||
self._protocol = protocol
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
return self._protocol
|
||||
|
||||
|
||||
def runProtocolsWithReactor(
|
||||
reactorBuilder, serverProtocol, clientProtocol, endpointCreator
|
||||
):
|
||||
"""
|
||||
Connect two protocols using endpoints and a new reactor instance.
|
||||
|
||||
A new reactor will be created and run, with the client and server protocol
|
||||
instances connected to each other using the given endpoint creator. The
|
||||
protocols should run through some set of tests, then disconnect; when both
|
||||
have disconnected the reactor will be stopped and the function will
|
||||
return.
|
||||
|
||||
@param reactorBuilder: A L{ReactorBuilder} instance.
|
||||
|
||||
@param serverProtocol: A L{ConnectableProtocol} that will be the server.
|
||||
|
||||
@param clientProtocol: A L{ConnectableProtocol} that will be the client.
|
||||
|
||||
@param endpointCreator: An instance of L{EndpointCreator}.
|
||||
|
||||
@return: The reactor run by this test.
|
||||
"""
|
||||
reactor = reactorBuilder.buildReactor()
|
||||
serverProtocol._setAttributes(reactor, Deferred())
|
||||
clientProtocol._setAttributes(reactor, Deferred())
|
||||
serverFactory = _SingleProtocolFactory(serverProtocol)
|
||||
clientFactory = _SingleProtocolFactory(clientProtocol)
|
||||
|
||||
# Listen on a port:
|
||||
serverEndpoint = endpointCreator.server(reactor)
|
||||
d = serverEndpoint.listen(serverFactory)
|
||||
|
||||
# Connect to the port:
|
||||
def gotPort(p):
|
||||
clientEndpoint = endpointCreator.client(reactor, p.getHost())
|
||||
return clientEndpoint.connect(clientFactory)
|
||||
|
||||
d.addCallback(gotPort)
|
||||
|
||||
# Stop reactor when both connections are lost:
|
||||
def failed(result):
|
||||
log.err(result, "Connection setup failed.")
|
||||
|
||||
disconnected = gatherResults([serverProtocol._done, clientProtocol._done])
|
||||
d.addCallback(lambda _: disconnected)
|
||||
d.addErrback(failed)
|
||||
d.addCallback(lambda _: needsRunningReactor(reactor, reactor.stop))
|
||||
|
||||
reactorBuilder.runReactor(reactor)
|
||||
return reactor
|
||||
|
||||
|
||||
def _getWriters(reactor):
|
||||
"""
|
||||
Like L{IReactorFDSet.getWriters}, but with support for IOCP reactor as
|
||||
well.
|
||||
"""
|
||||
if IReactorFDSet.providedBy(reactor):
|
||||
return reactor.getWriters()
|
||||
elif "IOCP" in reactor.__class__.__name__:
|
||||
return reactor.handles
|
||||
else:
|
||||
# Cannot tell what is going on.
|
||||
raise Exception(f"Cannot find writers on {reactor!r}")
|
||||
|
||||
|
||||
class _AcceptOneClient(ServerFactory):
|
||||
"""
|
||||
This factory fires a L{Deferred} with a protocol instance shortly after it
|
||||
is constructed (hopefully long enough afterwards so that it has been
|
||||
connected to a transport).
|
||||
|
||||
@ivar reactor: The reactor used to schedule the I{shortly}.
|
||||
|
||||
@ivar result: A L{Deferred} which will be fired with the protocol instance.
|
||||
"""
|
||||
|
||||
def __init__(self, reactor, result):
|
||||
self.reactor = reactor
|
||||
self.result = result
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
protocol = ServerFactory.buildProtocol(self, addr)
|
||||
self.reactor.callLater(0, self.result.callback, protocol)
|
||||
return protocol
|
||||
|
||||
|
||||
class _SimplePullProducer:
|
||||
"""
|
||||
A pull producer which writes one byte whenever it is resumed. For use by
|
||||
C{test_unregisterProducerAfterDisconnect}.
|
||||
"""
|
||||
|
||||
def __init__(self, consumer):
|
||||
self.consumer = consumer
|
||||
|
||||
def stopProducing(self):
|
||||
pass
|
||||
|
||||
def resumeProducing(self):
|
||||
log.msg("Producer.resumeProducing")
|
||||
self.consumer.write(b"x")
|
||||
|
||||
|
||||
class Stop(ClientFactory):
|
||||
"""
|
||||
A client factory which stops a reactor when a connection attempt fails.
|
||||
"""
|
||||
|
||||
failReason = None
|
||||
|
||||
def __init__(self, reactor):
|
||||
self.reactor = reactor
|
||||
|
||||
def clientConnectionFailed(self, connector, reason):
|
||||
self.failReason = reason
|
||||
msg(f"Stop(CF) cCFailed: {reason.getErrorMessage()}")
|
||||
self.reactor.stop()
|
||||
|
||||
|
||||
class ClosingLaterProtocol(ConnectableProtocol):
|
||||
"""
|
||||
ClosingLaterProtocol exchanges one byte with its peer and then disconnects
|
||||
itself. This is mostly a work-around for the fact that connectionMade is
|
||||
called before the SSL handshake has completed.
|
||||
"""
|
||||
|
||||
def __init__(self, onConnectionLost):
|
||||
self.lostConnectionReason = None
|
||||
self.onConnectionLost = onConnectionLost
|
||||
|
||||
def connectionMade(self):
|
||||
msg("ClosingLaterProtocol.connectionMade")
|
||||
|
||||
def dataReceived(self, bytes):
|
||||
msg(f"ClosingLaterProtocol.dataReceived {bytes!r}")
|
||||
self.transport.loseConnection()
|
||||
|
||||
def connectionLost(self, reason):
|
||||
msg("ClosingLaterProtocol.connectionLost")
|
||||
self.lostConnectionReason = reason
|
||||
self.onConnectionLost.callback(self)
|
||||
|
||||
|
||||
class ConnectionTestsMixin:
|
||||
"""
|
||||
This mixin defines test methods which should apply to most L{ITransport}
|
||||
implementations.
|
||||
"""
|
||||
|
||||
endpoints: Optional[EndpointCreator] = None
|
||||
|
||||
def test_logPrefix(self):
|
||||
"""
|
||||
Client and server transports implement L{ILoggingContext.logPrefix} to
|
||||
return a message reflecting the protocol they are running.
|
||||
"""
|
||||
|
||||
class CustomLogPrefixProtocol(ConnectableProtocol):
|
||||
def __init__(self, prefix):
|
||||
self._prefix = prefix
|
||||
self.system = None
|
||||
|
||||
def connectionMade(self):
|
||||
self.transport.write(b"a")
|
||||
|
||||
def logPrefix(self):
|
||||
return self._prefix
|
||||
|
||||
def dataReceived(self, bytes):
|
||||
self.system = context.get(ILogContext)["system"]
|
||||
self.transport.write(b"b")
|
||||
# Only close connection if both sides have received data, so
|
||||
# that both sides have system set.
|
||||
if b"b" in bytes:
|
||||
self.transport.loseConnection()
|
||||
|
||||
client = CustomLogPrefixProtocol("Custom Client")
|
||||
server = CustomLogPrefixProtocol("Custom Server")
|
||||
runProtocolsWithReactor(self, server, client, self.endpoints)
|
||||
self.assertIn("Custom Client", client.system)
|
||||
self.assertIn("Custom Server", server.system)
|
||||
|
||||
def test_writeAfterDisconnect(self):
|
||||
"""
|
||||
After a connection is disconnected, L{ITransport.write} and
|
||||
L{ITransport.writeSequence} are no-ops.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
|
||||
finished = []
|
||||
|
||||
serverConnectionLostDeferred = Deferred()
|
||||
protocol = lambda: ClosingLaterProtocol(serverConnectionLostDeferred)
|
||||
portDeferred = self.endpoints.server(reactor).listen(
|
||||
ServerFactory.forProtocol(protocol)
|
||||
)
|
||||
|
||||
def listening(port):
|
||||
msg(f"Listening on {port.getHost()!r}")
|
||||
endpoint = self.endpoints.client(reactor, port.getHost())
|
||||
|
||||
lostConnectionDeferred = Deferred()
|
||||
protocol = lambda: ClosingLaterProtocol(lostConnectionDeferred)
|
||||
client = endpoint.connect(ClientFactory.forProtocol(protocol))
|
||||
|
||||
def write(proto):
|
||||
msg(f"About to write to {proto!r}")
|
||||
proto.transport.write(b"x")
|
||||
|
||||
client.addCallbacks(write, lostConnectionDeferred.errback)
|
||||
|
||||
def disconnected(proto):
|
||||
msg(f"{proto!r} disconnected")
|
||||
proto.transport.write(b"some bytes to get lost")
|
||||
proto.transport.writeSequence([b"some", b"more"])
|
||||
finished.append(True)
|
||||
|
||||
lostConnectionDeferred.addCallback(disconnected)
|
||||
serverConnectionLostDeferred.addCallback(disconnected)
|
||||
return gatherResults([lostConnectionDeferred, serverConnectionLostDeferred])
|
||||
|
||||
def onListen():
|
||||
portDeferred.addCallback(listening)
|
||||
portDeferred.addErrback(err)
|
||||
portDeferred.addCallback(lambda ignored: reactor.stop())
|
||||
|
||||
needsRunningReactor(reactor, onListen)
|
||||
|
||||
self.runReactor(reactor)
|
||||
self.assertEqual(finished, [True, True])
|
||||
|
||||
def test_protocolGarbageAfterLostConnection(self):
|
||||
"""
|
||||
After the connection a protocol is being used for is closed, the
|
||||
reactor discards all of its references to the protocol.
|
||||
"""
|
||||
lostConnectionDeferred = Deferred()
|
||||
clientProtocol = ClosingLaterProtocol(lostConnectionDeferred)
|
||||
clientRef = ref(clientProtocol)
|
||||
|
||||
reactor = self.buildReactor()
|
||||
portDeferred = self.endpoints.server(reactor).listen(
|
||||
ServerFactory.forProtocol(Protocol)
|
||||
)
|
||||
|
||||
def listening(port):
|
||||
msg(f"Listening on {port.getHost()!r}")
|
||||
endpoint = self.endpoints.client(reactor, port.getHost())
|
||||
|
||||
client = endpoint.connect(ClientFactory.forProtocol(lambda: clientProtocol))
|
||||
|
||||
def disconnect(proto):
|
||||
msg(f"About to disconnect {proto!r}")
|
||||
proto.transport.loseConnection()
|
||||
|
||||
client.addCallback(disconnect)
|
||||
client.addErrback(lostConnectionDeferred.errback)
|
||||
return lostConnectionDeferred
|
||||
|
||||
def onListening():
|
||||
portDeferred.addCallback(listening)
|
||||
portDeferred.addErrback(err)
|
||||
portDeferred.addBoth(lambda ignored: reactor.stop())
|
||||
|
||||
needsRunningReactor(reactor, onListening)
|
||||
|
||||
self.runReactor(reactor)
|
||||
|
||||
# Drop the reference and get the garbage collector to tell us if there
|
||||
# are no references to the protocol instance left in the reactor.
|
||||
clientProtocol = None
|
||||
collect()
|
||||
self.assertIsNone(clientRef())
|
||||
|
||||
|
||||
class LogObserverMixin:
|
||||
"""
|
||||
Mixin for L{TestCase} subclasses which want to observe log events.
|
||||
"""
|
||||
|
||||
def observe(self):
|
||||
loggedMessages = []
|
||||
log.addObserver(loggedMessages.append)
|
||||
self.addCleanup(log.removeObserver, loggedMessages.append)
|
||||
return loggedMessages
|
||||
|
||||
|
||||
class BrokenContextFactory:
|
||||
"""
|
||||
A context factory with a broken C{getContext} method, for exercising the
|
||||
error handling for such a case.
|
||||
"""
|
||||
|
||||
message = "Some path was wrong maybe"
|
||||
|
||||
def getContext(self):
|
||||
raise ValueError(self.message)
|
||||
|
||||
|
||||
class StreamClientTestsMixin:
|
||||
"""
|
||||
This mixin defines tests applicable to SOCK_STREAM client implementations.
|
||||
|
||||
This must be mixed in to a L{ReactorBuilder
|
||||
<twisted.internet.test.reactormixins.ReactorBuilder>} subclass, as it
|
||||
depends on several of its methods.
|
||||
|
||||
Then the methods C{connect} and C{listen} must defined, defining a client
|
||||
and a server communicating with each other.
|
||||
"""
|
||||
|
||||
def test_interface(self):
|
||||
"""
|
||||
The C{connect} method returns an object providing L{IConnector}.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
connector = self.connect(reactor, ClientFactory())
|
||||
self.assertTrue(verifyObject(IConnector, connector))
|
||||
|
||||
def test_clientConnectionFailedStopsReactor(self):
|
||||
"""
|
||||
The reactor can be stopped by a client factory's
|
||||
C{clientConnectionFailed} method.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
needsRunningReactor(reactor, lambda: self.connect(reactor, Stop(reactor)))
|
||||
self.runReactor(reactor)
|
||||
|
||||
def test_connectEvent(self):
|
||||
"""
|
||||
This test checks that we correctly get notifications event for a
|
||||
client. This ought to prevent a regression under Windows using the
|
||||
GTK2 reactor. See #3925.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
|
||||
self.listen(reactor, ServerFactory.forProtocol(Protocol))
|
||||
connected = []
|
||||
|
||||
class CheckConnection(Protocol):
|
||||
def connectionMade(self):
|
||||
connected.append(self)
|
||||
reactor.stop()
|
||||
|
||||
clientFactory = Stop(reactor)
|
||||
clientFactory.protocol = CheckConnection
|
||||
|
||||
needsRunningReactor(reactor, lambda: self.connect(reactor, clientFactory))
|
||||
|
||||
reactor.run()
|
||||
|
||||
self.assertTrue(connected)
|
||||
|
||||
def test_unregisterProducerAfterDisconnect(self):
|
||||
"""
|
||||
If a producer is unregistered from a transport after the transport has
|
||||
been disconnected (by the peer) and after C{loseConnection} has been
|
||||
called, the transport is not re-added to the reactor as a writer as
|
||||
would be necessary if the transport were still connected.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
self.listen(reactor, ServerFactory.forProtocol(ClosingProtocol))
|
||||
|
||||
finished = Deferred()
|
||||
finished.addErrback(log.err)
|
||||
finished.addCallback(lambda ign: reactor.stop())
|
||||
|
||||
writing = []
|
||||
|
||||
class ClientProtocol(Protocol):
|
||||
"""
|
||||
Protocol to connect, register a producer, try to lose the
|
||||
connection, wait for the server to disconnect from us, and then
|
||||
unregister the producer.
|
||||
"""
|
||||
|
||||
def connectionMade(self):
|
||||
log.msg("ClientProtocol.connectionMade")
|
||||
self.transport.registerProducer(
|
||||
_SimplePullProducer(self.transport), False
|
||||
)
|
||||
self.transport.loseConnection()
|
||||
|
||||
def connectionLost(self, reason):
|
||||
log.msg("ClientProtocol.connectionLost")
|
||||
self.unregister()
|
||||
writing.append(self.transport in _getWriters(reactor))
|
||||
finished.callback(None)
|
||||
|
||||
def unregister(self):
|
||||
log.msg("ClientProtocol unregister")
|
||||
self.transport.unregisterProducer()
|
||||
|
||||
clientFactory = ClientFactory()
|
||||
clientFactory.protocol = ClientProtocol
|
||||
self.connect(reactor, clientFactory)
|
||||
self.runReactor(reactor)
|
||||
self.assertFalse(writing[0], "Transport was writing after unregisterProducer.")
|
||||
|
||||
def test_disconnectWhileProducing(self):
|
||||
"""
|
||||
If C{loseConnection} is called while a producer is registered with the
|
||||
transport, the connection is closed after the producer is unregistered.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
|
||||
# For some reason, pyobject/pygtk will not deliver the close
|
||||
# notification that should happen after the unregisterProducer call in
|
||||
# this test. The selectable is in the write notification set, but no
|
||||
# notification ever arrives. Probably for the same reason #5233 led
|
||||
# win32eventreactor to be broken.
|
||||
skippedReactors = ["Glib2Reactor", "Gtk2Reactor"]
|
||||
reactorClassName = reactor.__class__.__name__
|
||||
if reactorClassName in skippedReactors and platform.isWindows():
|
||||
raise SkipTest(
|
||||
"A pygobject/pygtk bug disables this functionality " "on Windows."
|
||||
)
|
||||
|
||||
class Producer:
|
||||
def resumeProducing(self):
|
||||
log.msg("Producer.resumeProducing")
|
||||
|
||||
self.listen(reactor, ServerFactory.forProtocol(Protocol))
|
||||
|
||||
finished = Deferred()
|
||||
finished.addErrback(log.err)
|
||||
finished.addCallback(lambda ign: reactor.stop())
|
||||
|
||||
class ClientProtocol(Protocol):
|
||||
"""
|
||||
Protocol to connect, register a producer, try to lose the
|
||||
connection, unregister the producer, and wait for the connection to
|
||||
actually be lost.
|
||||
"""
|
||||
|
||||
def connectionMade(self):
|
||||
log.msg("ClientProtocol.connectionMade")
|
||||
self.transport.registerProducer(Producer(), False)
|
||||
self.transport.loseConnection()
|
||||
# Let the reactor tick over, in case synchronously calling
|
||||
# loseConnection and then unregisterProducer is the same as
|
||||
# synchronously calling unregisterProducer and then
|
||||
# loseConnection (as it is in several reactors).
|
||||
reactor.callLater(0, reactor.callLater, 0, self.unregister)
|
||||
|
||||
def unregister(self):
|
||||
log.msg("ClientProtocol unregister")
|
||||
self.transport.unregisterProducer()
|
||||
# This should all be pretty quick. Fail the test
|
||||
# if we don't get a connectionLost event really
|
||||
# soon.
|
||||
reactor.callLater(
|
||||
1.0, finished.errback, Failure(Exception("Connection was not lost"))
|
||||
)
|
||||
|
||||
def connectionLost(self, reason):
|
||||
log.msg("ClientProtocol.connectionLost")
|
||||
finished.callback(None)
|
||||
|
||||
clientFactory = ClientFactory()
|
||||
clientFactory.protocol = ClientProtocol
|
||||
self.connect(reactor, clientFactory)
|
||||
self.runReactor(reactor)
|
||||
# If the test failed, we logged an error already and trial
|
||||
# will catch it.
|
||||
@@ -0,0 +1,49 @@
|
||||
|
||||
This is a concatenation of thing1.pem and thing2.pem.
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID6DCCAtACAwtEVjANBgkqhkiG9w0BAQsFADCBtzELMAkGA1UEBhMCVFIxDzAN
|
||||
BgNVBAgMBsOHb3J1bTEUMBIGA1UEBwwLQmHFn21ha8OnxLExEjAQBgNVBAMMCWxv
|
||||
Y2FsaG9zdDEcMBoGA1UECgwTVHdpc3RlZCBNYXRyaXggTGFiczEkMCIGA1UECwwb
|
||||
QXV0b21hdGVkIFRlc3RpbmcgQXV0aG9yaXR5MSkwJwYJKoZIhvcNAQkBFhpzZWN1
|
||||
cml0eUB0d2lzdGVkbWF0cml4LmNvbTAgFw0yMjA4MjMyMzUxMTVaGA8yMTIyMDcz
|
||||
MDIzNTExNVowgbcxCzAJBgNVBAYTAlRSMQ8wDQYDVQQIDAbDh29ydW0xFDASBgNV
|
||||
BAcMC0JhxZ9tYWvDp8SxMRIwEAYDVQQDDAlsb2NhbGhvc3QxHDAaBgNVBAoME1R3
|
||||
aXN0ZWQgTWF0cml4IExhYnMxJDAiBgNVBAsMG0F1dG9tYXRlZCBUZXN0aW5nIEF1
|
||||
dGhvcml0eTEpMCcGCSqGSIb3DQEJARYac2VjdXJpdHlAdHdpc3RlZG1hdHJpeC5j
|
||||
b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDxEgrHBfUQuzIyDaZM
|
||||
RqMy7h8tFCaQ4+0EshqENOjf4AMpxFeyxdHhR0IPBvMDZ7FkWg/mh8NImD2BgfhC
|
||||
Z8fuWIfUCmF/sA2BInfwMwJAKy28g6wpg+ZJGpyadKq0+OrN/fmT3wsaEP/wcOuD
|
||||
Pqk6wKt6Ry7eF3p7obgHHVVyku7gGQ/8bxshWNtoFT8oyCsO54VluEnL2XTBKQsS
|
||||
EQQKCk0RdIAo2kCuA4AE+SxFlCBp9XiIux6a/z1rHTewuuCeM3DgxTgHDstUHMFO
|
||||
YF29JlESeBvBpjiKCKrJk8K+Szhwza4eSfoB+dTenZX9Z/u370tUvppkC4Gf62lL
|
||||
fqndAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAERd0Gt9An/6gKqOaEvTvHIubSIi
|
||||
BCN4/udreXSH19z32DksCPW9sTG93O7PX51T2GN0FKgF2AcNSl5aNpKxQexn3uBJ
|
||||
F4nxM4AGv0ltkHzeJdltyCVQyzcYOxAHAGTTNWqaWsJezXngirpvFRE15OaJcMRA
|
||||
M5ygRh52YKYS+DvhaRwPs5xsTSLaJtyGYmXoXu8zTcvqVyWWdqj4PEHkV/g7OFoS
|
||||
Mc+0s22i7FMvMRJozHA8hHJv4Dg6it6ifvQiZh6ihEO+kTSb1cpDfyu3Uhw50dAW
|
||||
23/mit+5faDT5g6lC5AG3yU/DOWFwJqXi73YhcggqTtWBufQfavq/2QmdXw=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID6DCCAtACAwtEVjANBgkqhkiG9w0BAQsFADCBtzELMAkGA1UEBhMCVFIxDzAN
|
||||
BgNVBAgMBsOHb3J1bTEUMBIGA1UEBwwLQmHFn21ha8OnxLExEjAQBgNVBAMMCWxv
|
||||
Y2FsaG9zdDEcMBoGA1UECgwTVHdpc3RlZCBNYXRyaXggTGFiczEkMCIGA1UECwwb
|
||||
QXV0b21hdGVkIFRlc3RpbmcgQXV0aG9yaXR5MSkwJwYJKoZIhvcNAQkBFhpzZWN1
|
||||
cml0eUB0d2lzdGVkbWF0cml4LmNvbTAgFw0yMjA4MjMyMzUxMzdaGA8yMTIyMDcz
|
||||
MDIzNTEzN1owgbcxCzAJBgNVBAYTAlRSMQ8wDQYDVQQIDAbDh29ydW0xFDASBgNV
|
||||
BAcMC0JhxZ9tYWvDp8SxMRIwEAYDVQQDDAlsb2NhbGhvc3QxHDAaBgNVBAoME1R3
|
||||
aXN0ZWQgTWF0cml4IExhYnMxJDAiBgNVBAsMG0F1dG9tYXRlZCBUZXN0aW5nIEF1
|
||||
dGhvcml0eTEpMCcGCSqGSIb3DQEJARYac2VjdXJpdHlAdHdpc3RlZG1hdHJpeC5j
|
||||
b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCB/ICkiDmAALdHzezV
|
||||
fxg6/azHX9eymmBS2PEizw0QOkt+VriWu5Ik0KHDBP9NWuLiQF3G1Zch6YVkOfPU
|
||||
vbvfCv4GbFdssnoVPX/0VOT/YEsEHtyCjgGo22FAE/vd4EcnnJx017B/5mzxLaf6
|
||||
u9XwOZaIojw6I20iaKDjpfyvVBJA5gXH1YGrBYRLIqGaS4lbZcTy6ZoqanYMOmJn
|
||||
kd3IBOJOaFwfazROVvaVkQQlP9SgAKwaEcEF8Hk/E4N5nVWS8QnN/PWhzouJSAtQ
|
||||
WzXgLTZrUdDItBxlG+IeVSWdYUoBPFruZ33tdZta7zJ2FrrkxX27azd+nCIC6NLQ
|
||||
h1DfAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGmx33QaOIw+dTzdBmPGZy5kZv6Y
|
||||
a1z/rfyBtnR37iAZ+5rhJZkcQSLyUY+kiuEPxTouh3Amx8EOVccerfZvzCFAb21L
|
||||
TOeSoeJ1TSn2ppIBqzjjdJ/JHcQTMEB/5D8S069Rp/D6wbgCbmJ2CJcU+OYY+W7z
|
||||
fT6fEy7MmNXC3RwoD5Vc0RaS4fDSCMOG/bL/rylQoimmvm4rQAmZJK2PuKvqtkzc
|
||||
Fs9dq4VJ3Eba1/qA2000CtzHHBYMalL9+EQxwjy0QmVL08usUSMiRPfySCUJBEYA
|
||||
xZmEEvyHkD7dFVngJvjW64pFg7d/mV0e8dQv1QURoCDb7XOuCWY1vltp7iE=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1 @@
|
||||
This file is not a certificate; it is present to make sure that it will be skipped.
|
||||
@@ -0,0 +1,27 @@
|
||||
This certificate, and thing2.pem, are generated by re-running
|
||||
twisted/test/thing1.pem and copying the certificate out (without its private
|
||||
key) a few times.
|
||||
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID6DCCAtACAwtEVjANBgkqhkiG9w0BAQsFADCBtzELMAkGA1UEBhMCVFIxDzAN
|
||||
BgNVBAgMBsOHb3J1bTEUMBIGA1UEBwwLQmHFn21ha8OnxLExEjAQBgNVBAMMCWxv
|
||||
Y2FsaG9zdDEcMBoGA1UECgwTVHdpc3RlZCBNYXRyaXggTGFiczEkMCIGA1UECwwb
|
||||
QXV0b21hdGVkIFRlc3RpbmcgQXV0aG9yaXR5MSkwJwYJKoZIhvcNAQkBFhpzZWN1
|
||||
cml0eUB0d2lzdGVkbWF0cml4LmNvbTAgFw0yMjA4MjMyMzUxMTVaGA8yMTIyMDcz
|
||||
MDIzNTExNVowgbcxCzAJBgNVBAYTAlRSMQ8wDQYDVQQIDAbDh29ydW0xFDASBgNV
|
||||
BAcMC0JhxZ9tYWvDp8SxMRIwEAYDVQQDDAlsb2NhbGhvc3QxHDAaBgNVBAoME1R3
|
||||
aXN0ZWQgTWF0cml4IExhYnMxJDAiBgNVBAsMG0F1dG9tYXRlZCBUZXN0aW5nIEF1
|
||||
dGhvcml0eTEpMCcGCSqGSIb3DQEJARYac2VjdXJpdHlAdHdpc3RlZG1hdHJpeC5j
|
||||
b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDxEgrHBfUQuzIyDaZM
|
||||
RqMy7h8tFCaQ4+0EshqENOjf4AMpxFeyxdHhR0IPBvMDZ7FkWg/mh8NImD2BgfhC
|
||||
Z8fuWIfUCmF/sA2BInfwMwJAKy28g6wpg+ZJGpyadKq0+OrN/fmT3wsaEP/wcOuD
|
||||
Pqk6wKt6Ry7eF3p7obgHHVVyku7gGQ/8bxshWNtoFT8oyCsO54VluEnL2XTBKQsS
|
||||
EQQKCk0RdIAo2kCuA4AE+SxFlCBp9XiIux6a/z1rHTewuuCeM3DgxTgHDstUHMFO
|
||||
YF29JlESeBvBpjiKCKrJk8K+Szhwza4eSfoB+dTenZX9Z/u370tUvppkC4Gf62lL
|
||||
fqndAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAERd0Gt9An/6gKqOaEvTvHIubSIi
|
||||
BCN4/udreXSH19z32DksCPW9sTG93O7PX51T2GN0FKgF2AcNSl5aNpKxQexn3uBJ
|
||||
F4nxM4AGv0ltkHzeJdltyCVQyzcYOxAHAGTTNWqaWsJezXngirpvFRE15OaJcMRA
|
||||
M5ygRh52YKYS+DvhaRwPs5xsTSLaJtyGYmXoXu8zTcvqVyWWdqj4PEHkV/g7OFoS
|
||||
Mc+0s22i7FMvMRJozHA8hHJv4Dg6it6ifvQiZh6ihEO+kTSb1cpDfyu3Uhw50dAW
|
||||
23/mit+5faDT5g6lC5AG3yU/DOWFwJqXi73YhcggqTtWBufQfavq/2QmdXw=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,23 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID6DCCAtACAwtEVjANBgkqhkiG9w0BAQsFADCBtzELMAkGA1UEBhMCVFIxDzAN
|
||||
BgNVBAgMBsOHb3J1bTEUMBIGA1UEBwwLQmHFn21ha8OnxLExEjAQBgNVBAMMCWxv
|
||||
Y2FsaG9zdDEcMBoGA1UECgwTVHdpc3RlZCBNYXRyaXggTGFiczEkMCIGA1UECwwb
|
||||
QXV0b21hdGVkIFRlc3RpbmcgQXV0aG9yaXR5MSkwJwYJKoZIhvcNAQkBFhpzZWN1
|
||||
cml0eUB0d2lzdGVkbWF0cml4LmNvbTAgFw0yMjA4MjMyMzUxMzdaGA8yMTIyMDcz
|
||||
MDIzNTEzN1owgbcxCzAJBgNVBAYTAlRSMQ8wDQYDVQQIDAbDh29ydW0xFDASBgNV
|
||||
BAcMC0JhxZ9tYWvDp8SxMRIwEAYDVQQDDAlsb2NhbGhvc3QxHDAaBgNVBAoME1R3
|
||||
aXN0ZWQgTWF0cml4IExhYnMxJDAiBgNVBAsMG0F1dG9tYXRlZCBUZXN0aW5nIEF1
|
||||
dGhvcml0eTEpMCcGCSqGSIb3DQEJARYac2VjdXJpdHlAdHdpc3RlZG1hdHJpeC5j
|
||||
b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCB/ICkiDmAALdHzezV
|
||||
fxg6/azHX9eymmBS2PEizw0QOkt+VriWu5Ik0KHDBP9NWuLiQF3G1Zch6YVkOfPU
|
||||
vbvfCv4GbFdssnoVPX/0VOT/YEsEHtyCjgGo22FAE/vd4EcnnJx017B/5mzxLaf6
|
||||
u9XwOZaIojw6I20iaKDjpfyvVBJA5gXH1YGrBYRLIqGaS4lbZcTy6ZoqanYMOmJn
|
||||
kd3IBOJOaFwfazROVvaVkQQlP9SgAKwaEcEF8Hk/E4N5nVWS8QnN/PWhzouJSAtQ
|
||||
WzXgLTZrUdDItBxlG+IeVSWdYUoBPFruZ33tdZta7zJ2FrrkxX27azd+nCIC6NLQ
|
||||
h1DfAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGmx33QaOIw+dTzdBmPGZy5kZv6Y
|
||||
a1z/rfyBtnR37iAZ+5rhJZkcQSLyUY+kiuEPxTouh3Amx8EOVccerfZvzCFAb21L
|
||||
TOeSoeJ1TSn2ppIBqzjjdJ/JHcQTMEB/5D8S069Rp/D6wbgCbmJ2CJcU+OYY+W7z
|
||||
fT6fEy7MmNXC3RwoD5Vc0RaS4fDSCMOG/bL/rylQoimmvm4rQAmZJK2PuKvqtkzc
|
||||
Fs9dq4VJ3Eba1/qA2000CtzHHBYMalL9+EQxwjy0QmVL08usUSMiRPfySCUJBEYA
|
||||
xZmEEvyHkD7dFVngJvjW64pFg7d/mV0e8dQv1QURoCDb7XOuCWY1vltp7iE=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,23 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIID6DCCAtACAwtEVjANBgkqhkiG9w0BAQsFADCBtzELMAkGA1UEBhMCVFIxDzAN
|
||||
BgNVBAgMBsOHb3J1bTEUMBIGA1UEBwwLQmHFn21ha8OnxLExEjAQBgNVBAMMCWxv
|
||||
Y2FsaG9zdDEcMBoGA1UECgwTVHdpc3RlZCBNYXRyaXggTGFiczEkMCIGA1UECwwb
|
||||
QXV0b21hdGVkIFRlc3RpbmcgQXV0aG9yaXR5MSkwJwYJKoZIhvcNAQkBFhpzZWN1
|
||||
cml0eUB0d2lzdGVkbWF0cml4LmNvbTAgFw0yMjA4MjMyMzUxMzdaGA8yMTIyMDcz
|
||||
MDIzNTEzN1owgbcxCzAJBgNVBAYTAlRSMQ8wDQYDVQQIDAbDh29ydW0xFDASBgNV
|
||||
BAcMC0JhxZ9tYWvDp8SxMRIwEAYDVQQDDAlsb2NhbGhvc3QxHDAaBgNVBAoME1R3
|
||||
aXN0ZWQgTWF0cml4IExhYnMxJDAiBgNVBAsMG0F1dG9tYXRlZCBUZXN0aW5nIEF1
|
||||
dGhvcml0eTEpMCcGCSqGSIb3DQEJARYac2VjdXJpdHlAdHdpc3RlZG1hdHJpeC5j
|
||||
b20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCB/ICkiDmAALdHzezV
|
||||
fxg6/azHX9eymmBS2PEizw0QOkt+VriWu5Ik0KHDBP9NWuLiQF3G1Zch6YVkOfPU
|
||||
vbvfCv4GbFdssnoVPX/0VOT/YEsEHtyCjgGo22FAE/vd4EcnnJx017B/5mzxLaf6
|
||||
u9XwOZaIojw6I20iaKDjpfyvVBJA5gXH1YGrBYRLIqGaS4lbZcTy6ZoqanYMOmJn
|
||||
kd3IBOJOaFwfazROVvaVkQQlP9SgAKwaEcEF8Hk/E4N5nVWS8QnN/PWhzouJSAtQ
|
||||
WzXgLTZrUdDItBxlG+IeVSWdYUoBPFruZ33tdZta7zJ2FrrkxX27azd+nCIC6NLQ
|
||||
h1DfAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAGmx33QaOIw+dTzdBmPGZy5kZv6Y
|
||||
a1z/rfyBtnR37iAZ+5rhJZkcQSLyUY+kiuEPxTouh3Amx8EOVccerfZvzCFAb21L
|
||||
TOeSoeJ1TSn2ppIBqzjjdJ/JHcQTMEB/5D8S069Rp/D6wbgCbmJ2CJcU+OYY+W7z
|
||||
fT6fEy7MmNXC3RwoD5Vc0RaS4fDSCMOG/bL/rylQoimmvm4rQAmZJK2PuKvqtkzc
|
||||
Fs9dq4VJ3Eba1/qA2000CtzHHBYMalL9+EQxwjy0QmVL08usUSMiRPfySCUJBEYA
|
||||
xZmEEvyHkD7dFVngJvjW64pFg7d/mV0e8dQv1QURoCDb7XOuCWY1vltp7iE=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -0,0 +1,63 @@
|
||||
# -*- test-case-name: twisted.internet.test.test_endpoints -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Fake client and server endpoint string parser plugins for testing purposes.
|
||||
"""
|
||||
|
||||
|
||||
from zope.interface.declarations import implementer
|
||||
|
||||
from twisted.internet.interfaces import (
|
||||
IStreamClientEndpoint,
|
||||
IStreamClientEndpointStringParserWithReactor,
|
||||
IStreamServerEndpoint,
|
||||
IStreamServerEndpointStringParser,
|
||||
)
|
||||
from twisted.plugin import IPlugin
|
||||
|
||||
|
||||
@implementer(IPlugin)
|
||||
class PluginBase:
|
||||
def __init__(self, pfx):
|
||||
self.prefix = pfx
|
||||
|
||||
|
||||
@implementer(IStreamClientEndpointStringParserWithReactor)
|
||||
class FakeClientParserWithReactor(PluginBase):
|
||||
def parseStreamClient(self, *a, **kw):
|
||||
return StreamClient(self, a, kw)
|
||||
|
||||
|
||||
@implementer(IStreamServerEndpointStringParser)
|
||||
class FakeParser(PluginBase):
|
||||
def parseStreamServer(self, *a, **kw):
|
||||
return StreamServer(self, a, kw)
|
||||
|
||||
|
||||
class EndpointBase:
|
||||
def __init__(self, parser, args, kwargs):
|
||||
self.parser = parser
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
|
||||
@implementer(IStreamClientEndpoint)
|
||||
class StreamClient(EndpointBase):
|
||||
def connect(self, protocolFactory=None):
|
||||
# IStreamClientEndpoint.connect
|
||||
pass
|
||||
|
||||
|
||||
@implementer(IStreamServerEndpoint)
|
||||
class StreamServer(EndpointBase):
|
||||
def listen(self, protocolFactory=None):
|
||||
# IStreamClientEndpoint.listen
|
||||
pass
|
||||
|
||||
|
||||
# Instantiate plugin interface providers to register them.
|
||||
fake = FakeParser("fake")
|
||||
fakeClientWithReactor = FakeClientParserWithReactor("crfake")
|
||||
fakeClientWithReactorAndPreference = FakeClientParserWithReactor("cpfake")
|
||||
@@ -0,0 +1,61 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Testing helpers related to the module system.
|
||||
"""
|
||||
|
||||
|
||||
__all__ = ["NoReactor", "AlternateReactor"]
|
||||
|
||||
import sys
|
||||
|
||||
import twisted.internet
|
||||
from twisted.test.test_twisted import SetAsideModule
|
||||
|
||||
|
||||
class NoReactor(SetAsideModule):
|
||||
"""
|
||||
Context manager that uninstalls the reactor, if any, and then restores it
|
||||
afterwards.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
SetAsideModule.__init__(self, "twisted.internet.reactor")
|
||||
|
||||
def __enter__(self):
|
||||
SetAsideModule.__enter__(self)
|
||||
if "twisted.internet.reactor" in self.modules:
|
||||
del twisted.internet.reactor
|
||||
|
||||
def __exit__(self, excType, excValue, traceback):
|
||||
SetAsideModule.__exit__(self, excType, excValue, traceback)
|
||||
# Clean up 'reactor' attribute that may have been set on
|
||||
# twisted.internet:
|
||||
reactor = self.modules.get("twisted.internet.reactor", None)
|
||||
if reactor is not None:
|
||||
twisted.internet.reactor = reactor
|
||||
else:
|
||||
try:
|
||||
del twisted.internet.reactor
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
|
||||
class AlternateReactor(NoReactor):
|
||||
"""
|
||||
A context manager which temporarily installs a different object as the
|
||||
global reactor.
|
||||
"""
|
||||
|
||||
def __init__(self, reactor):
|
||||
"""
|
||||
@param reactor: Any object to install as the global reactor.
|
||||
"""
|
||||
NoReactor.__init__(self)
|
||||
self.alternate = reactor
|
||||
|
||||
def __enter__(self):
|
||||
NoReactor.__enter__(self)
|
||||
twisted.internet.reactor = self.alternate
|
||||
sys.modules["twisted.internet.reactor"] = self.alternate
|
||||
@@ -0,0 +1,22 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
# On Windows, stdout is not opened in binary mode by default,
|
||||
# so newline characters are munged on writing, interfering with
|
||||
# the tests.
|
||||
import msvcrt
|
||||
|
||||
msvcrt.setmode( # type:ignore[attr-defined]
|
||||
sys.stdout.fileno(), os.O_BINARY
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
# Loop over each of the arguments given and print it to stdout
|
||||
for arg in sys.argv[1:]:
|
||||
res = arg + chr(0)
|
||||
|
||||
sys.stdout.buffer.write(res.encode(sys.getfilesystemencoding(), "surrogateescape"))
|
||||
sys.stdout.flush()
|
||||
@@ -0,0 +1,8 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
while 1:
|
||||
line = sys.stdin.readline().strip()
|
||||
if not line:
|
||||
break
|
||||
os.close(int(line))
|
||||
@@ -0,0 +1,25 @@
|
||||
import sys
|
||||
|
||||
# Override theSystemPath so it throws KeyError on gi.pygtkcompat:
|
||||
from twisted.python import modules
|
||||
from twisted.python.reflect import requireModule
|
||||
|
||||
modules.theSystemPath = modules.PythonPath([], moduleDict={})
|
||||
|
||||
# Now, when we import gireactor it shouldn't use pygtkcompat, and should
|
||||
# instead prevent gobject from being importable:
|
||||
gireactor = requireModule("twisted.internet.gireactor")
|
||||
for name in gireactor._PYGTK_MODULES:
|
||||
if sys.modules[name] is not None:
|
||||
sys.stdout.write(
|
||||
"failure, sys.modules[%r] is %r, instead of None"
|
||||
% (name, sys.modules["gobject"])
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
import gobject
|
||||
except ImportError:
|
||||
sys.stdout.write("success")
|
||||
else:
|
||||
sys.stdout.write(f"failure: {gobject.__path__} was imported")
|
||||
@@ -0,0 +1,46 @@
|
||||
# A program which exits after starting a child which inherits its
|
||||
# stdin/stdout/stderr and keeps them open until stdin is closed.
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def grandchild() -> None:
|
||||
sys.stdout.write("grandchild started")
|
||||
sys.stdout.flush()
|
||||
sys.stdin.read()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if sys.argv[1] == "child":
|
||||
if sys.argv[2] == "windows":
|
||||
import win32api as api
|
||||
import win32process as proc
|
||||
|
||||
info = proc.STARTUPINFO()
|
||||
info.hStdInput = api.GetStdHandle(api.STD_INPUT_HANDLE)
|
||||
info.hStdOutput = api.GetStdHandle(api.STD_OUTPUT_HANDLE)
|
||||
info.hStdError = api.GetStdHandle(api.STD_ERROR_HANDLE)
|
||||
python = sys.executable
|
||||
scriptDir = os.path.dirname(__file__)
|
||||
scriptName = os.path.basename(__file__)
|
||||
proc.CreateProcess(
|
||||
None,
|
||||
" ".join((python, scriptName, "grandchild")),
|
||||
None,
|
||||
None,
|
||||
1,
|
||||
0,
|
||||
os.environ,
|
||||
scriptDir,
|
||||
info,
|
||||
)
|
||||
else:
|
||||
if os.fork() == 0:
|
||||
grandchild()
|
||||
else:
|
||||
grandchild()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,418 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Utilities for unit testing reactor implementations.
|
||||
|
||||
The main feature of this module is L{ReactorBuilder}, a base class for use when
|
||||
writing interface/blackbox tests for reactor implementations. Test case classes
|
||||
for reactor features should subclass L{ReactorBuilder} instead of
|
||||
L{SynchronousTestCase}. All of the features of L{SynchronousTestCase} will be
|
||||
available. Additionally, the tests will automatically be applied to all
|
||||
available reactor implementations.
|
||||
"""
|
||||
|
||||
|
||||
__all__ = ["TestTimeoutError", "ReactorBuilder", "needsRunningReactor"]
|
||||
|
||||
import os
|
||||
import signal
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Callable, Dict, Optional, Sequence, Type, Union, cast
|
||||
|
||||
from zope.interface import Interface
|
||||
|
||||
from twisted.python import log
|
||||
from twisted.python.deprecate import _fullyQualifiedName as fullyQualifiedName
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.reflect import namedAny
|
||||
from twisted.python.runtime import platform
|
||||
from twisted.trial.unittest import SkipTest, SynchronousTestCase
|
||||
from twisted.trial.util import DEFAULT_TIMEOUT_DURATION, acquireAttribute
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Only bring in this name to support the type annotation below. We don't
|
||||
# really want to import a reactor module this early at runtime.
|
||||
from twisted.internet import asyncioreactor
|
||||
|
||||
# Access private APIs.
|
||||
try:
|
||||
from twisted.internet import process as _process
|
||||
except ImportError:
|
||||
process = None
|
||||
else:
|
||||
process = _process
|
||||
|
||||
|
||||
class TestTimeoutError(Exception):
|
||||
"""
|
||||
The reactor was still running after the timeout period elapsed in
|
||||
L{ReactorBuilder.runReactor}.
|
||||
"""
|
||||
|
||||
|
||||
def needsRunningReactor(reactor, thunk):
|
||||
"""
|
||||
Various functions within these tests need an already-running reactor at
|
||||
some point. They need to stop the reactor when the test has completed, and
|
||||
that means calling reactor.stop(). However, reactor.stop() raises an
|
||||
exception if the reactor isn't already running, so if the L{Deferred} that
|
||||
a particular API under test returns fires synchronously (as especially an
|
||||
endpoint's C{connect()} method may do, if the connect is to a local
|
||||
interface address) then the test won't be able to stop the reactor being
|
||||
tested and finish. So this calls C{thunk} only once C{reactor} is running.
|
||||
|
||||
(This is just an alias for
|
||||
L{twisted.internet.interfaces.IReactorCore.callWhenRunning} on the given
|
||||
reactor parameter, in order to centrally reference the above paragraph and
|
||||
repeating it everywhere as a comment.)
|
||||
|
||||
@param reactor: the L{twisted.internet.interfaces.IReactorCore} under test
|
||||
|
||||
@param thunk: a 0-argument callable, which eventually finishes the test in
|
||||
question, probably in a L{Deferred} callback.
|
||||
"""
|
||||
reactor.callWhenRunning(thunk)
|
||||
|
||||
|
||||
def stopOnError(case, reactor, publisher=None):
|
||||
"""
|
||||
Stop the reactor as soon as any error is logged on the given publisher.
|
||||
|
||||
This is beneficial for tests which will wait for a L{Deferred} to fire
|
||||
before completing (by passing or failing). Certain implementation bugs may
|
||||
prevent the L{Deferred} from firing with any result at all (consider a
|
||||
protocol's {dataReceived} method that raises an exception: this exception
|
||||
is logged but it won't ever cause a L{Deferred} to fire). In that case the
|
||||
test would have to complete by timing out which is a much less desirable
|
||||
outcome than completing as soon as the unexpected error is encountered.
|
||||
|
||||
@param case: A L{SynchronousTestCase} to use to clean up the necessary log
|
||||
observer when the test is over.
|
||||
@param reactor: The reactor to stop.
|
||||
@param publisher: A L{LogPublisher} to watch for errors. If L{None}, the
|
||||
global log publisher will be watched.
|
||||
"""
|
||||
if publisher is None:
|
||||
from twisted.python import log as publisher
|
||||
running = [None]
|
||||
|
||||
def stopIfError(event):
|
||||
if running and event.get("isError"):
|
||||
running.pop()
|
||||
reactor.stop()
|
||||
|
||||
publisher.addObserver(stopIfError)
|
||||
case.addCleanup(publisher.removeObserver, stopIfError)
|
||||
|
||||
|
||||
class ReactorBuilder:
|
||||
"""
|
||||
L{SynchronousTestCase} mixin which provides a reactor-creation API. This
|
||||
mixin defines C{setUp} and C{tearDown}, so mix it in before
|
||||
L{SynchronousTestCase} or call its methods from the overridden ones in the
|
||||
subclass.
|
||||
|
||||
@cvar skippedReactors: A dict mapping FQPN strings of reactors for
|
||||
which the tests defined by this class will be skipped to strings
|
||||
giving the skip message.
|
||||
@cvar requiredInterfaces: A C{list} of interfaces which the reactor must
|
||||
provide or these tests will be skipped. The default, L{None}, means
|
||||
that no interfaces are required.
|
||||
@ivar reactorFactory: A no-argument callable which returns the reactor to
|
||||
use for testing.
|
||||
@ivar originalHandler: The SIGCHLD handler which was installed when setUp
|
||||
ran and which will be re-installed when tearDown runs.
|
||||
@ivar _reactors: A list of FQPN strings giving the reactors for which
|
||||
L{SynchronousTestCase}s will be created.
|
||||
"""
|
||||
|
||||
_reactors = [
|
||||
# Select works everywhere
|
||||
"twisted.internet.selectreactor.SelectReactor",
|
||||
]
|
||||
|
||||
if platform.isWindows():
|
||||
# PortableGtkReactor is only really interesting on Windows,
|
||||
# but not really Windows specific; if you want you can
|
||||
# temporarily move this up to the all-platforms list to test
|
||||
# it on other platforms. It's not there in general because
|
||||
# it's not _really_ worth it to support on other platforms,
|
||||
# since no one really wants to use it on other platforms.
|
||||
_reactors.extend(
|
||||
[
|
||||
"twisted.internet.gireactor.PortableGIReactor",
|
||||
"twisted.internet.win32eventreactor.Win32Reactor",
|
||||
"twisted.internet.iocpreactor.reactor.IOCPReactor",
|
||||
]
|
||||
)
|
||||
else:
|
||||
_reactors.extend(
|
||||
[
|
||||
"twisted.internet.gireactor.GIReactor",
|
||||
]
|
||||
)
|
||||
|
||||
_reactors.append("twisted.internet.test.reactormixins.AsyncioSelectorReactor")
|
||||
_reactors.append("twisted.internet._threadedselect.ThreadedSelectReactor")
|
||||
|
||||
if platform.isMacOSX():
|
||||
_reactors.append("twisted.internet.cfreactor.CFReactor")
|
||||
else:
|
||||
_reactors.extend(
|
||||
[
|
||||
"twisted.internet.pollreactor.PollReactor",
|
||||
"twisted.internet.epollreactor.EPollReactor",
|
||||
]
|
||||
)
|
||||
if not platform.isLinux():
|
||||
# Presumably Linux is not going to start supporting kqueue, so
|
||||
# skip even trying this configuration.
|
||||
_reactors.extend(
|
||||
[
|
||||
# Support KQueue on non-OS-X POSIX platforms for now.
|
||||
"twisted.internet.kqreactor.KQueueReactor",
|
||||
]
|
||||
)
|
||||
|
||||
reactorFactory: Optional[Callable[[], object]] = None
|
||||
|
||||
originalHandler = None
|
||||
requiredInterfaces: Optional[Sequence[Type[Interface]]] = None
|
||||
skippedReactors: Dict[str, str] = {}
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Clear the SIGCHLD handler, if there is one, to ensure an environment
|
||||
like the one which exists prior to a call to L{reactor.run}.
|
||||
"""
|
||||
if not platform.isWindows():
|
||||
self.originalHandler = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
|
||||
|
||||
def tearDown(self):
|
||||
"""
|
||||
Restore the original SIGCHLD handler and reap processes as long as
|
||||
there seem to be any remaining.
|
||||
"""
|
||||
if self.originalHandler is not None:
|
||||
signal.signal(signal.SIGCHLD, self.originalHandler)
|
||||
if process is not None:
|
||||
begin = time.time()
|
||||
while process.reapProcessHandlers:
|
||||
log.msg(
|
||||
"ReactorBuilder.tearDown reaping some processes %r"
|
||||
% (process.reapProcessHandlers,)
|
||||
)
|
||||
process.reapAllProcesses()
|
||||
|
||||
# The process should exit on its own. However, if it
|
||||
# doesn't, we're stuck in this loop forever. To avoid
|
||||
# hanging the test suite, eventually give the process some
|
||||
# help exiting and move on.
|
||||
time.sleep(0.001)
|
||||
if time.time() - begin > 60:
|
||||
for pid in process.reapProcessHandlers:
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
raise Exception(
|
||||
"Timeout waiting for child processes to exit: %r"
|
||||
% (process.reapProcessHandlers,)
|
||||
)
|
||||
|
||||
def _unbuildReactor(self, reactor):
|
||||
"""
|
||||
Clean up any resources which may have been allocated for the given
|
||||
reactor by its creation or by a test which used it.
|
||||
"""
|
||||
# Chris says:
|
||||
#
|
||||
# XXX These explicit calls to clean up the waker (and any other
|
||||
# internal readers) should become obsolete when bug #3063 is
|
||||
# fixed. -radix, 2008-02-29. Fortunately it should probably cause an
|
||||
# error when bug #3063 is fixed, so it should be removed in the same
|
||||
# branch that fixes it.
|
||||
#
|
||||
# -exarkun
|
||||
reactor._uninstallHandler()
|
||||
if getattr(reactor, "_internalReaders", None) is not None:
|
||||
for reader in reactor._internalReaders:
|
||||
reactor.removeReader(reader)
|
||||
reader.connectionLost(None)
|
||||
reactor._internalReaders.clear()
|
||||
|
||||
# Here's an extra thing unrelated to wakers but necessary for
|
||||
# cleaning up after the reactors we make. -exarkun
|
||||
reactor.disconnectAll()
|
||||
|
||||
# It would also be bad if any timed calls left over were allowed to
|
||||
# run.
|
||||
calls = reactor.getDelayedCalls()
|
||||
for c in calls:
|
||||
c.cancel()
|
||||
|
||||
# Restore the original reactor state:
|
||||
from twisted.internet import reactor as globalReactor
|
||||
|
||||
globalReactor.__dict__ = reactor._originalReactorDict
|
||||
globalReactor.__class__ = reactor._originalReactorClass
|
||||
|
||||
def buildReactor(self):
|
||||
"""
|
||||
Create and return a reactor using C{self.reactorFactory}.
|
||||
"""
|
||||
try:
|
||||
from twisted.internet import reactor as globalReactor
|
||||
from twisted.internet.cfreactor import CFReactor
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
if (
|
||||
isinstance(globalReactor, CFReactor)
|
||||
and self.reactorFactory is CFReactor
|
||||
):
|
||||
raise SkipTest(
|
||||
"CFReactor uses APIs which manipulate global state, "
|
||||
"so it's not safe to run its own reactor-builder tests "
|
||||
"under itself"
|
||||
)
|
||||
try:
|
||||
assert self.reactorFactory is not None
|
||||
reactor = self.reactorFactory()
|
||||
reactor._originalReactorDict = globalReactor.__dict__
|
||||
reactor._originalReactorClass = globalReactor.__class__
|
||||
# Make twisted.internet.reactor point to the new reactor,
|
||||
# temporarily; this is undone in unbuildReactor().
|
||||
globalReactor.__dict__ = reactor.__dict__
|
||||
globalReactor.__class__ = reactor.__class__
|
||||
except BaseException:
|
||||
# Unfortunately, not all errors which result in a reactor
|
||||
# being unusable are detectable without actually
|
||||
# instantiating the reactor. So we catch some more here
|
||||
# and skip the test if necessary. We also log it to aid
|
||||
# with debugging, but flush the logged error so the test
|
||||
# doesn't fail.
|
||||
log.err(None, "Failed to install reactor")
|
||||
self.flushLoggedErrors()
|
||||
raise SkipTest(Failure().getErrorMessage())
|
||||
else:
|
||||
if self.requiredInterfaces is not None:
|
||||
missing = [
|
||||
required
|
||||
for required in self.requiredInterfaces
|
||||
if not required.providedBy(reactor)
|
||||
]
|
||||
if missing:
|
||||
self._unbuildReactor(reactor)
|
||||
raise SkipTest(
|
||||
"%s does not provide %s"
|
||||
% (
|
||||
fullyQualifiedName(reactor.__class__),
|
||||
",".join([fullyQualifiedName(x) for x in missing]),
|
||||
)
|
||||
)
|
||||
self.addCleanup(self._unbuildReactor, reactor)
|
||||
return reactor
|
||||
|
||||
def getTimeout(self):
|
||||
"""
|
||||
Determine how long to run the test before considering it failed.
|
||||
|
||||
@return: A C{int} or C{float} giving a number of seconds.
|
||||
"""
|
||||
return acquireAttribute(self._parents, "timeout", DEFAULT_TIMEOUT_DURATION)
|
||||
|
||||
def runReactor(self, reactor, timeout=None):
|
||||
"""
|
||||
Run the reactor for at most the given amount of time.
|
||||
|
||||
@param reactor: The reactor to run.
|
||||
|
||||
@type timeout: C{int} or C{float}
|
||||
@param timeout: The maximum amount of time, specified in seconds, to
|
||||
allow the reactor to run. If the reactor is still running after
|
||||
this much time has elapsed, it will be stopped and an exception
|
||||
raised. If L{None}, the default test method timeout imposed by
|
||||
Trial will be used. This depends on the L{IReactorTime}
|
||||
implementation of C{reactor} for correct operation.
|
||||
|
||||
@raise TestTimeoutError: If the reactor is still running after
|
||||
C{timeout} seconds.
|
||||
"""
|
||||
if timeout is None:
|
||||
timeout = self.getTimeout()
|
||||
|
||||
timedOut = []
|
||||
|
||||
def stop():
|
||||
timedOut.append(None)
|
||||
reactor.stop()
|
||||
|
||||
timedOutCall = reactor.callLater(timeout, stop)
|
||||
reactor.run()
|
||||
if timedOut:
|
||||
raise TestTimeoutError(f"reactor still running after {timeout} seconds")
|
||||
else:
|
||||
timedOutCall.cancel()
|
||||
|
||||
@classmethod
|
||||
def makeTestCaseClasses(
|
||||
cls: Type["ReactorBuilder"],
|
||||
) -> Dict[str, Union[Type["ReactorBuilder"], Type[SynchronousTestCase]]]:
|
||||
"""
|
||||
Create a L{SynchronousTestCase} subclass which mixes in C{cls} for each
|
||||
known reactor and return a dict mapping their names to them.
|
||||
"""
|
||||
classes: Dict[
|
||||
str, Union[Type["ReactorBuilder"], Type[SynchronousTestCase]]
|
||||
] = {}
|
||||
for reactor in cls._reactors:
|
||||
shortReactorName = reactor.split(".")[-1]
|
||||
name = (cls.__name__ + "." + shortReactorName + "Tests").replace(".", "_")
|
||||
|
||||
class testcase(cls, SynchronousTestCase): # type: ignore[valid-type,misc]
|
||||
__module__ = cls.__module__
|
||||
if reactor in cls.skippedReactors:
|
||||
skip = cls.skippedReactors[reactor]
|
||||
try:
|
||||
reactorFactory = namedAny(reactor)
|
||||
except BaseException:
|
||||
skip = Failure().getErrorMessage()
|
||||
|
||||
testcase.__name__ = name
|
||||
testcase.__qualname__ = ".".join(cls.__qualname__.split()[0:-1] + [name])
|
||||
classes[testcase.__name__] = testcase
|
||||
return classes
|
||||
|
||||
|
||||
def asyncioSelectorReactor(self: object) -> "asyncioreactor.AsyncioSelectorReactor":
|
||||
"""
|
||||
Make a new asyncio reactor associated with a new event loop.
|
||||
|
||||
The test suite prefers this constructor because having a new event loop
|
||||
for each reactor provides better test isolation. The real constructor
|
||||
prefers to re-use (or create) a global loop because of how this interacts
|
||||
with other asyncio-based libraries and applications (though maybe it
|
||||
shouldn't).
|
||||
|
||||
@param self: The L{ReactorBuilder} subclass this is being called on. We
|
||||
don't use this parameter but we get called with it anyway.
|
||||
"""
|
||||
from asyncio import get_event_loop, new_event_loop, set_event_loop
|
||||
|
||||
from twisted.internet import asyncioreactor
|
||||
|
||||
asTestCase = cast(SynchronousTestCase, self)
|
||||
originalLoop = get_event_loop()
|
||||
loop = new_event_loop()
|
||||
set_event_loop(loop)
|
||||
|
||||
@asTestCase.addCleanup
|
||||
def cleanUp():
|
||||
loop.close()
|
||||
set_event_loop(originalLoop)
|
||||
|
||||
return asyncioreactor.AsyncioSelectorReactor(loop)
|
||||
|
||||
|
||||
# Give it an alias that makes the names of the generated test classes fit the
|
||||
# pattern.
|
||||
AsyncioSelectorReactor = asyncioSelectorReactor
|
||||
@@ -0,0 +1,144 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.internet.abstract}, a collection of APIs for implementing
|
||||
reactors.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Union
|
||||
|
||||
from hypothesis import example, given, strategies as st
|
||||
|
||||
from twisted.internet.abstract import FileDescriptor, isIPv6Address
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
from .test_tcp import _FakeFDSetReactor
|
||||
|
||||
|
||||
class IPv6AddressTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{isIPv6Address}, a function for determining if a particular
|
||||
string is an IPv6 address literal.
|
||||
"""
|
||||
|
||||
def test_empty(self) -> None:
|
||||
"""
|
||||
The empty string is not an IPv6 address literal.
|
||||
"""
|
||||
self.assertFalse(isIPv6Address(""))
|
||||
|
||||
def test_colon(self) -> None:
|
||||
"""
|
||||
A single C{":"} is not an IPv6 address literal.
|
||||
"""
|
||||
self.assertFalse(isIPv6Address(":"))
|
||||
|
||||
def test_loopback(self) -> None:
|
||||
"""
|
||||
C{"::1"} is the IPv6 loopback address literal.
|
||||
"""
|
||||
self.assertTrue(isIPv6Address("::1"))
|
||||
|
||||
def test_scopeID(self) -> None:
|
||||
"""
|
||||
An otherwise valid IPv6 address literal may also include a C{"%"}
|
||||
followed by an arbitrary scope identifier.
|
||||
"""
|
||||
self.assertTrue(isIPv6Address("fe80::1%eth0"))
|
||||
self.assertTrue(isIPv6Address("fe80::2%1"))
|
||||
self.assertTrue(isIPv6Address("fe80::3%en2"))
|
||||
|
||||
def test_invalidWithScopeID(self) -> None:
|
||||
"""
|
||||
An otherwise invalid IPv6 address literal is still invalid with a
|
||||
trailing scope identifier.
|
||||
"""
|
||||
self.assertFalse(isIPv6Address("%eth0"))
|
||||
self.assertFalse(isIPv6Address(":%eth0"))
|
||||
self.assertFalse(isIPv6Address("hello%eth0"))
|
||||
|
||||
def test_unicodeAndBytes(self) -> None:
|
||||
"""
|
||||
L{isIPv6Address} evaluates ASCII-encoded bytes as well as text.
|
||||
"""
|
||||
# the type annotation only supports str, but bytes is supported at
|
||||
# runtime
|
||||
self.assertTrue(isIPv6Address(b"fe80::2%1")) # type: ignore[arg-type]
|
||||
self.assertTrue(isIPv6Address("fe80::2%1"))
|
||||
self.assertFalse(isIPv6Address("\u4321"))
|
||||
self.assertFalse(isIPv6Address("hello%eth0"))
|
||||
self.assertFalse(isIPv6Address(b"hello%eth0")) # type: ignore[arg-type]
|
||||
|
||||
|
||||
class TrackingFileDescriptor(FileDescriptor):
|
||||
"""
|
||||
Write a limited amount, and track what gets written.
|
||||
"""
|
||||
|
||||
# Annoying implementation details we need to make it work:
|
||||
connected = True
|
||||
_writeDisconnected = False
|
||||
|
||||
def __init__(
|
||||
self, operations: list[Union[int, bytes]], written: list[bytes], send_limit: int
|
||||
):
|
||||
self.operations = operations
|
||||
self.written = written
|
||||
self.SEND_LIMIT = send_limit
|
||||
FileDescriptor.__init__(self, _FakeFDSetReactor())
|
||||
|
||||
def writeSomeData(self, data: bytes) -> int:
|
||||
toWrite = self.operations.pop(0)
|
||||
assert isinstance(toWrite, int)
|
||||
toWrite = min(toWrite, len(data))
|
||||
self.written.append(data[:toWrite])
|
||||
return toWrite
|
||||
|
||||
|
||||
class WriteBufferingTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for the complex logic in the L{FileDescriptor} class.
|
||||
"""
|
||||
|
||||
@given(
|
||||
operations=st.lists(
|
||||
st.one_of(
|
||||
st.binary(min_size=1, max_size=10),
|
||||
st.integers(min_value=0, max_value=10),
|
||||
),
|
||||
min_size=3,
|
||||
max_size=30,
|
||||
)
|
||||
)
|
||||
# This catches a bug that was introduced by a performance refactoring:
|
||||
@example(operations=[b"abcdef", 0, b"g"])
|
||||
def test_writeBuffering(self, operations: list[Union[bytes, int]]) -> None:
|
||||
"""
|
||||
A sequence of C{write()} and C{doWrite()} will eventually write all the
|
||||
data correctly and in order.
|
||||
|
||||
@param operations: A list of C{bytes} (indicating a C{write()}) or
|
||||
C{int} (indicating C{doWrite()} with the integer being how much
|
||||
C{writeSomeData()} writeSomeData will successfully write).
|
||||
"""
|
||||
expected = b"".join(op for op in operations if isinstance(op, bytes))
|
||||
written: list[bytes] = []
|
||||
|
||||
# Send at most 5 bytes per call to writeSomeData(); default is much
|
||||
# higher, of course, but made it smaller so we can have faster
|
||||
# tests.
|
||||
SEND_LIMIT = 5
|
||||
fd = TrackingFileDescriptor(operations, written, SEND_LIMIT)
|
||||
|
||||
# Make sure we flush whatever is left at the end:
|
||||
operations += [SEND_LIMIT * 2] * (1 + len(expected) // SEND_LIMIT)
|
||||
|
||||
while operations:
|
||||
if isinstance(operations[0], bytes):
|
||||
fd.write(operations.pop(0)) # type: ignore[arg-type]
|
||||
else:
|
||||
fd.doWrite()
|
||||
|
||||
result = b"".join(written)
|
||||
self.assertEqual(expected, result)
|
||||
@@ -0,0 +1,249 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
import os
|
||||
import socket
|
||||
from unittest import skipIf
|
||||
|
||||
from twisted.internet.address import (
|
||||
HostnameAddress,
|
||||
IPv4Address,
|
||||
IPv6Address,
|
||||
UNIXAddress,
|
||||
)
|
||||
from twisted.python.compat import nativeString
|
||||
from twisted.python.runtime import platform
|
||||
from twisted.trial.unittest import SynchronousTestCase, TestCase
|
||||
|
||||
symlinkSkip = not platform._supportsSymlinks()
|
||||
|
||||
try:
|
||||
socket.AF_UNIX
|
||||
except AttributeError:
|
||||
unixSkip = True
|
||||
else:
|
||||
unixSkip = False
|
||||
|
||||
|
||||
class AddressTestCaseMixin:
|
||||
def test_addressComparison(self):
|
||||
"""
|
||||
Two different address instances, sharing the same properties are
|
||||
considered equal by C{==} and not considered not equal by C{!=}.
|
||||
|
||||
Note: When applied via UNIXAddress class, this uses the same
|
||||
filename for both objects being compared.
|
||||
"""
|
||||
self.assertTrue(self.buildAddress() == self.buildAddress())
|
||||
self.assertFalse(self.buildAddress() != self.buildAddress())
|
||||
|
||||
def test_hash(self):
|
||||
"""
|
||||
C{__hash__} can be used to get a hash of an address, allowing
|
||||
addresses to be used as keys in dictionaries, for instance.
|
||||
"""
|
||||
addr = self.buildAddress()
|
||||
d = {addr: True}
|
||||
self.assertTrue(d[self.buildAddress()])
|
||||
|
||||
def test_differentNamesComparison(self):
|
||||
"""
|
||||
Check that comparison operators work correctly on address objects
|
||||
when a different name is passed in
|
||||
"""
|
||||
self.assertFalse(self.buildAddress() == self.buildDifferentAddress())
|
||||
self.assertFalse(self.buildDifferentAddress() == self.buildAddress())
|
||||
|
||||
self.assertTrue(self.buildAddress() != self.buildDifferentAddress())
|
||||
self.assertTrue(self.buildDifferentAddress() != self.buildAddress())
|
||||
|
||||
def assertDeprecations(self, testMethod, message):
|
||||
"""
|
||||
Assert that the a DeprecationWarning with the given message was
|
||||
emitted against the given method.
|
||||
"""
|
||||
warnings = self.flushWarnings([testMethod])
|
||||
self.assertEqual(warnings[0]["category"], DeprecationWarning)
|
||||
self.assertEqual(warnings[0]["message"], message)
|
||||
self.assertEqual(len(warnings), 1)
|
||||
|
||||
|
||||
class IPv4AddressTestCaseMixin(AddressTestCaseMixin):
|
||||
addressArgSpec = (("type", "%s"), ("host", "%r"), ("port", "%d"))
|
||||
|
||||
|
||||
class HostnameAddressTests(TestCase, AddressTestCaseMixin):
|
||||
"""
|
||||
Test case for L{HostnameAddress}.
|
||||
"""
|
||||
|
||||
addressArgSpec = (("hostname", "%s"), ("port", "%d"))
|
||||
|
||||
def buildAddress(self):
|
||||
"""
|
||||
Create an arbitrary new L{HostnameAddress} instance.
|
||||
|
||||
@return: A L{HostnameAddress} instance.
|
||||
"""
|
||||
return HostnameAddress(b"example.com", 0)
|
||||
|
||||
def buildDifferentAddress(self):
|
||||
"""
|
||||
Like L{buildAddress}, but with a different hostname.
|
||||
|
||||
@return: A L{HostnameAddress} instance.
|
||||
"""
|
||||
return HostnameAddress(b"example.net", 0)
|
||||
|
||||
|
||||
class IPv4AddressTCPTests(SynchronousTestCase, IPv4AddressTestCaseMixin):
|
||||
def buildAddress(self):
|
||||
"""
|
||||
Create an arbitrary new L{IPv4Address} instance with a C{"TCP"}
|
||||
type. A new instance is created for each call, but always for the
|
||||
same address.
|
||||
"""
|
||||
return IPv4Address("TCP", "127.0.0.1", 0)
|
||||
|
||||
def buildDifferentAddress(self):
|
||||
"""
|
||||
Like L{buildAddress}, but with a different fixed address.
|
||||
"""
|
||||
return IPv4Address("TCP", "127.0.0.2", 0)
|
||||
|
||||
|
||||
class IPv4AddressUDPTests(SynchronousTestCase, IPv4AddressTestCaseMixin):
|
||||
def buildAddress(self):
|
||||
"""
|
||||
Create an arbitrary new L{IPv4Address} instance with a C{"UDP"}
|
||||
type. A new instance is created for each call, but always for the
|
||||
same address.
|
||||
"""
|
||||
return IPv4Address("UDP", "127.0.0.1", 0)
|
||||
|
||||
def buildDifferentAddress(self):
|
||||
"""
|
||||
Like L{buildAddress}, but with a different fixed address.
|
||||
"""
|
||||
return IPv4Address("UDP", "127.0.0.2", 0)
|
||||
|
||||
|
||||
class IPv6AddressTests(SynchronousTestCase, AddressTestCaseMixin):
|
||||
addressArgSpec = (("type", "%s"), ("host", "%r"), ("port", "%d"))
|
||||
|
||||
def buildAddress(self):
|
||||
"""
|
||||
Create an arbitrary new L{IPv6Address} instance with a C{"TCP"}
|
||||
type. A new instance is created for each call, but always for the
|
||||
same address.
|
||||
"""
|
||||
return IPv6Address("TCP", "::1", 0)
|
||||
|
||||
def buildDifferentAddress(self):
|
||||
"""
|
||||
Like L{buildAddress}, but with a different fixed address.
|
||||
"""
|
||||
return IPv6Address("TCP", "::2", 0)
|
||||
|
||||
|
||||
@skipIf(unixSkip, "Platform doesn't support UNIX sockets.")
|
||||
class UNIXAddressTests(SynchronousTestCase):
|
||||
addressArgSpec = (("name", "%r"),)
|
||||
|
||||
def setUp(self):
|
||||
self._socketAddress = self.mktemp()
|
||||
self._otherAddress = self.mktemp()
|
||||
|
||||
def buildAddress(self):
|
||||
"""
|
||||
Create an arbitrary new L{UNIXAddress} instance. A new instance is
|
||||
created for each call, but always for the same address.
|
||||
"""
|
||||
return UNIXAddress(self._socketAddress)
|
||||
|
||||
def buildDifferentAddress(self):
|
||||
"""
|
||||
Like L{buildAddress}, but with a different fixed address.
|
||||
"""
|
||||
return UNIXAddress(self._otherAddress)
|
||||
|
||||
def test_repr(self):
|
||||
"""
|
||||
The repr of L{UNIXAddress} returns with the filename that the
|
||||
L{UNIXAddress} is for.
|
||||
"""
|
||||
self.assertEqual(
|
||||
repr(self.buildAddress()),
|
||||
"UNIXAddress('%s')" % (nativeString(self._socketAddress)),
|
||||
)
|
||||
|
||||
@skipIf(symlinkSkip, "Platform does not support symlinks")
|
||||
def test_comparisonOfLinkedFiles(self):
|
||||
"""
|
||||
UNIXAddress objects compare as equal if they link to the same file.
|
||||
"""
|
||||
linkName = self.mktemp()
|
||||
with open(self._socketAddress, "w") as self.fd:
|
||||
os.symlink(os.path.abspath(self._socketAddress), linkName)
|
||||
self.assertEqual(UNIXAddress(self._socketAddress), UNIXAddress(linkName))
|
||||
self.assertEqual(UNIXAddress(linkName), UNIXAddress(self._socketAddress))
|
||||
|
||||
@skipIf(symlinkSkip, "Platform does not support symlinks")
|
||||
def test_hashOfLinkedFiles(self):
|
||||
"""
|
||||
UNIXAddress Objects that compare as equal have the same hash value.
|
||||
"""
|
||||
linkName = self.mktemp()
|
||||
with open(self._socketAddress, "w") as self.fd:
|
||||
os.symlink(os.path.abspath(self._socketAddress), linkName)
|
||||
self.assertEqual(
|
||||
hash(UNIXAddress(self._socketAddress)), hash(UNIXAddress(linkName))
|
||||
)
|
||||
|
||||
|
||||
@skipIf(unixSkip, "platform doesn't support UNIX sockets.")
|
||||
class EmptyUNIXAddressTests(SynchronousTestCase, AddressTestCaseMixin):
|
||||
"""
|
||||
Tests for L{UNIXAddress} operations involving a L{None} address.
|
||||
"""
|
||||
|
||||
addressArgSpec = (("name", "%r"),)
|
||||
|
||||
def setUp(self):
|
||||
self._socketAddress = self.mktemp()
|
||||
|
||||
def buildAddress(self):
|
||||
"""
|
||||
Create an arbitrary new L{UNIXAddress} instance. A new instance is
|
||||
created for each call, but always for the same address. This builds it
|
||||
with a fixed address of L{None}.
|
||||
"""
|
||||
return UNIXAddress(None)
|
||||
|
||||
def buildDifferentAddress(self):
|
||||
"""
|
||||
Like L{buildAddress}, but with a random temporary directory.
|
||||
"""
|
||||
return UNIXAddress(self._socketAddress)
|
||||
|
||||
@skipIf(symlinkSkip, "Platform does not support symlinks")
|
||||
def test_comparisonOfLinkedFiles(self):
|
||||
"""
|
||||
A UNIXAddress referring to a L{None} address does not
|
||||
compare equal to a UNIXAddress referring to a symlink.
|
||||
"""
|
||||
linkName = self.mktemp()
|
||||
with open(self._socketAddress, "w") as self.fd:
|
||||
os.symlink(os.path.abspath(self._socketAddress), linkName)
|
||||
self.assertNotEqual(UNIXAddress(self._socketAddress), UNIXAddress(None))
|
||||
self.assertNotEqual(UNIXAddress(None), UNIXAddress(self._socketAddress))
|
||||
|
||||
def test_emptyHash(self):
|
||||
"""
|
||||
C{__hash__} can be used to get a hash of an address, even one referring
|
||||
to L{None} rather than a real path.
|
||||
"""
|
||||
addr = self.buildAddress()
|
||||
d = {addr: True}
|
||||
self.assertTrue(d[self.buildAddress()])
|
||||
@@ -0,0 +1,296 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.internet.asyncioreactor}.
|
||||
"""
|
||||
import gc
|
||||
import sys
|
||||
from asyncio import (
|
||||
AbstractEventLoop,
|
||||
AbstractEventLoopPolicy,
|
||||
DefaultEventLoopPolicy,
|
||||
Future,
|
||||
SelectorEventLoop,
|
||||
get_event_loop,
|
||||
get_event_loop_policy,
|
||||
set_event_loop,
|
||||
set_event_loop_policy,
|
||||
)
|
||||
from unittest import skipIf
|
||||
|
||||
from twisted.internet.asyncioreactor import AsyncioSelectorReactor
|
||||
from twisted.python.runtime import platform
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
from .reactormixins import ReactorBuilder
|
||||
|
||||
hasWindowsProactorEventLoopPolicy = False
|
||||
hasWindowsSelectorEventLoopPolicy = False
|
||||
|
||||
try:
|
||||
if sys.platform.startswith("win32"):
|
||||
from asyncio import (
|
||||
WindowsProactorEventLoopPolicy,
|
||||
WindowsSelectorEventLoopPolicy,
|
||||
)
|
||||
|
||||
hasWindowsProactorEventLoopPolicy = True
|
||||
hasWindowsSelectorEventLoopPolicy = True
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
_defaultEventLoop = DefaultEventLoopPolicy().new_event_loop()
|
||||
_defaultEventLoopIsSelector = isinstance(_defaultEventLoop, SelectorEventLoop)
|
||||
_defaultEventLoop.close()
|
||||
|
||||
|
||||
class AsyncioSelectorReactorTests(ReactorBuilder, SynchronousTestCase):
|
||||
"""
|
||||
L{AsyncioSelectorReactor} tests.
|
||||
"""
|
||||
|
||||
def assertReactorWorksWithAsyncioFuture(self, reactor):
|
||||
"""
|
||||
Ensure that C{reactor} has an event loop that works
|
||||
properly with L{asyncio.Future}.
|
||||
"""
|
||||
future = Future()
|
||||
result = []
|
||||
|
||||
def completed(future):
|
||||
result.append(future.result())
|
||||
reactor.stop()
|
||||
|
||||
future.add_done_callback(completed)
|
||||
future.set_result(True)
|
||||
|
||||
self.assertEqual(result, [])
|
||||
self.runReactor(reactor, timeout=1)
|
||||
self.assertEqual(result, [True])
|
||||
|
||||
def newLoop(self, policy: AbstractEventLoopPolicy) -> AbstractEventLoop:
|
||||
"""
|
||||
Make a new asyncio loop from a policy for use with a reactor, and add
|
||||
appropriate cleanup to restore any global state.
|
||||
"""
|
||||
existingLoop = get_event_loop()
|
||||
existingPolicy = get_event_loop_policy()
|
||||
result = policy.new_event_loop()
|
||||
|
||||
@self.addCleanup
|
||||
def cleanUp():
|
||||
result.close()
|
||||
set_event_loop(existingLoop)
|
||||
set_event_loop_policy(existingPolicy)
|
||||
|
||||
return result
|
||||
|
||||
@skipIf(
|
||||
not _defaultEventLoopIsSelector,
|
||||
"default event loop: {}\nis not of type SelectorEventLoop "
|
||||
"on Python {}.{} ({})".format(
|
||||
type(_defaultEventLoop),
|
||||
sys.version_info.major,
|
||||
sys.version_info.minor,
|
||||
platform.getType(),
|
||||
),
|
||||
)
|
||||
def test_defaultSelectorEventLoopFromGlobalPolicy(self):
|
||||
"""
|
||||
L{AsyncioSelectorReactor} wraps the global policy's event loop
|
||||
by default. This ensures that L{asyncio.Future}s and
|
||||
coroutines created by library code that uses
|
||||
L{asyncio.get_event_loop} are bound to the same loop.
|
||||
"""
|
||||
reactor = AsyncioSelectorReactor()
|
||||
self.assertReactorWorksWithAsyncioFuture(reactor)
|
||||
|
||||
@skipIf(
|
||||
not _defaultEventLoopIsSelector,
|
||||
"default event loop: {}\nis not of type SelectorEventLoop "
|
||||
"on Python {}.{} ({})".format(
|
||||
type(_defaultEventLoop),
|
||||
sys.version_info.major,
|
||||
sys.version_info.minor,
|
||||
platform.getType(),
|
||||
),
|
||||
)
|
||||
def test_newSelectorEventLoopFromDefaultEventLoopPolicy(self):
|
||||
"""
|
||||
If we use the L{asyncio.DefaultLoopPolicy} to create a new event loop,
|
||||
and then pass that event loop to a new L{AsyncioSelectorReactor},
|
||||
this reactor should work properly with L{asyncio.Future}.
|
||||
"""
|
||||
event_loop = self.newLoop(DefaultEventLoopPolicy())
|
||||
reactor = AsyncioSelectorReactor(event_loop)
|
||||
set_event_loop(event_loop)
|
||||
self.assertReactorWorksWithAsyncioFuture(reactor)
|
||||
|
||||
@skipIf(
|
||||
_defaultEventLoopIsSelector,
|
||||
"default event loop: {}\nis of type SelectorEventLoop "
|
||||
"on Python {}.{} ({})".format(
|
||||
type(_defaultEventLoop),
|
||||
sys.version_info.major,
|
||||
sys.version_info.minor,
|
||||
platform.getType(),
|
||||
),
|
||||
)
|
||||
def test_defaultNotASelectorEventLoopFromGlobalPolicy(self):
|
||||
"""
|
||||
On Windows Python 3.5 to 3.7, L{get_event_loop()} returns a
|
||||
L{WindowsSelectorEventLoop} by default.
|
||||
On Windows Python 3.8+, L{get_event_loop()} returns a
|
||||
L{WindowsProactorEventLoop} by default.
|
||||
L{AsyncioSelectorReactor} should raise a
|
||||
L{TypeError} if the default event loop is not a
|
||||
L{WindowsSelectorEventLoop}.
|
||||
"""
|
||||
self.assertRaises(TypeError, AsyncioSelectorReactor)
|
||||
|
||||
@skipIf(
|
||||
not hasWindowsProactorEventLoopPolicy, "WindowsProactorEventLoop not available"
|
||||
)
|
||||
def test_WindowsProactorEventLoop(self):
|
||||
"""
|
||||
L{AsyncioSelectorReactor} will raise a L{TypeError}
|
||||
if instantiated with a L{asyncio.WindowsProactorEventLoop}
|
||||
"""
|
||||
event_loop = self.newLoop(WindowsProactorEventLoopPolicy())
|
||||
self.assertRaises(TypeError, AsyncioSelectorReactor, event_loop)
|
||||
|
||||
@skipIf(
|
||||
not hasWindowsSelectorEventLoopPolicy,
|
||||
"WindowsSelectorEventLoop only on Windows",
|
||||
)
|
||||
def test_WindowsSelectorEventLoop(self):
|
||||
"""
|
||||
L{WindowsSelectorEventLoop} works with L{AsyncioSelectorReactor}
|
||||
"""
|
||||
event_loop = self.newLoop(WindowsSelectorEventLoopPolicy())
|
||||
reactor = AsyncioSelectorReactor(event_loop)
|
||||
set_event_loop(event_loop)
|
||||
self.assertReactorWorksWithAsyncioFuture(reactor)
|
||||
|
||||
@skipIf(
|
||||
not hasWindowsProactorEventLoopPolicy,
|
||||
"WindowsProactorEventLoopPolicy only on Windows",
|
||||
)
|
||||
def test_WindowsProactorEventLoopPolicy(self):
|
||||
"""
|
||||
L{AsyncioSelectorReactor} will raise a L{TypeError}
|
||||
if L{asyncio.WindowsProactorEventLoopPolicy} is default.
|
||||
"""
|
||||
set_event_loop_policy(WindowsProactorEventLoopPolicy())
|
||||
self.addCleanup(lambda: set_event_loop_policy(None))
|
||||
with self.assertRaises(TypeError):
|
||||
AsyncioSelectorReactor()
|
||||
|
||||
@skipIf(
|
||||
not hasWindowsSelectorEventLoopPolicy,
|
||||
"WindowsSelectorEventLoopPolicy only on Windows",
|
||||
)
|
||||
def test_WindowsSelectorEventLoopPolicy(self):
|
||||
"""
|
||||
L{AsyncioSelectorReactor} will work if
|
||||
if L{asyncio.WindowsSelectorEventLoopPolicy} is default.
|
||||
"""
|
||||
set_event_loop_policy(WindowsSelectorEventLoopPolicy())
|
||||
self.addCleanup(lambda: set_event_loop_policy(None))
|
||||
reactor = AsyncioSelectorReactor()
|
||||
self.assertReactorWorksWithAsyncioFuture(reactor)
|
||||
|
||||
def test_seconds(self):
|
||||
"""L{seconds} should return a plausible epoch time."""
|
||||
if hasWindowsSelectorEventLoopPolicy:
|
||||
set_event_loop_policy(WindowsSelectorEventLoopPolicy())
|
||||
self.addCleanup(lambda: set_event_loop_policy(None))
|
||||
reactor = AsyncioSelectorReactor()
|
||||
result = reactor.seconds()
|
||||
|
||||
# greater than 2020-01-01
|
||||
self.assertGreater(result, 1577836800)
|
||||
|
||||
# less than 2120-01-01
|
||||
self.assertLess(result, 4733510400)
|
||||
|
||||
def test_delayedCallResetToLater(self):
|
||||
"""
|
||||
L{DelayedCall.reset()} properly reschedules timer to later time
|
||||
"""
|
||||
if hasWindowsSelectorEventLoopPolicy:
|
||||
set_event_loop_policy(WindowsSelectorEventLoopPolicy())
|
||||
self.addCleanup(lambda: set_event_loop_policy(None))
|
||||
|
||||
reactor = AsyncioSelectorReactor()
|
||||
|
||||
timer_called_at = [None]
|
||||
|
||||
def on_timer():
|
||||
timer_called_at[0] = reactor.seconds()
|
||||
|
||||
start_time = reactor.seconds()
|
||||
dc = reactor.callLater(0, on_timer)
|
||||
dc.reset(0.5)
|
||||
reactor.callLater(1, reactor.stop)
|
||||
reactor.run()
|
||||
|
||||
self.assertIsNotNone(timer_called_at[0])
|
||||
self.assertGreater(timer_called_at[0] - start_time, 0.4)
|
||||
|
||||
def test_delayedCallResetToEarlier(self):
|
||||
"""
|
||||
L{DelayedCall.reset()} properly reschedules timer to earlier time
|
||||
"""
|
||||
if hasWindowsSelectorEventLoopPolicy:
|
||||
set_event_loop_policy(WindowsSelectorEventLoopPolicy())
|
||||
reactor = AsyncioSelectorReactor()
|
||||
|
||||
timer_called_at = [None]
|
||||
|
||||
def on_timer():
|
||||
timer_called_at[0] = reactor.seconds()
|
||||
|
||||
start_time = reactor.seconds()
|
||||
dc = reactor.callLater(0.5, on_timer)
|
||||
dc.reset(0)
|
||||
reactor.callLater(1, reactor.stop)
|
||||
|
||||
import io
|
||||
from contextlib import redirect_stderr
|
||||
|
||||
stderr = io.StringIO()
|
||||
with redirect_stderr(stderr):
|
||||
reactor.run()
|
||||
|
||||
self.assertEqual(stderr.getvalue(), "")
|
||||
self.assertIsNotNone(timer_called_at[0])
|
||||
self.assertLess(timer_called_at[0] - start_time, 0.4)
|
||||
if hasWindowsSelectorEventLoopPolicy:
|
||||
set_event_loop_policy(None)
|
||||
|
||||
def test_noCycleReferencesInCallLater(self):
|
||||
"""
|
||||
L{AsyncioSelectorReactor.callLater()} doesn't leave cyclic references
|
||||
"""
|
||||
if hasWindowsSelectorEventLoopPolicy:
|
||||
set_event_loop_policy(WindowsSelectorEventLoopPolicy())
|
||||
gc_was_enabled = gc.isenabled()
|
||||
gc.disable()
|
||||
try:
|
||||
objects_before = len(gc.get_objects())
|
||||
timer_count = 1000
|
||||
|
||||
reactor = AsyncioSelectorReactor()
|
||||
for _ in range(timer_count):
|
||||
reactor.callLater(0, lambda: None)
|
||||
reactor.runUntilCurrent()
|
||||
|
||||
objects_after = len(gc.get_objects())
|
||||
self.assertLess((objects_after - objects_before) / timer_count, 1)
|
||||
finally:
|
||||
if gc_was_enabled:
|
||||
gc.enable()
|
||||
if hasWindowsSelectorEventLoopPolicy:
|
||||
set_event_loop_policy(None)
|
||||
@@ -0,0 +1,472 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.internet.base}.
|
||||
"""
|
||||
|
||||
import socket
|
||||
from queue import Queue
|
||||
from typing import Callable
|
||||
from unittest import skipIf
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
from twisted.internet._resolver import FirstOneWins
|
||||
from twisted.internet.base import DelayedCall, ReactorBase, ThreadedResolver
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.internet.error import DNSLookupError
|
||||
from twisted.internet.interfaces import IReactorThreads, IReactorTime, IResolverSimple
|
||||
from twisted.internet.task import Clock
|
||||
from twisted.python.threadpool import ThreadPool
|
||||
from twisted.trial.unittest import SkipTest, TestCase
|
||||
|
||||
try:
|
||||
import signal as _signal
|
||||
except ImportError:
|
||||
signal = None
|
||||
else:
|
||||
signal = _signal
|
||||
|
||||
_P = ParamSpec("_P")
|
||||
|
||||
|
||||
@implementer(IReactorTime, IReactorThreads)
|
||||
class FakeReactor:
|
||||
"""
|
||||
A fake reactor implementation which just supports enough reactor APIs for
|
||||
L{ThreadedResolver}.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._clock = Clock()
|
||||
self.callLater = self._clock.callLater
|
||||
|
||||
self._threadpool = ThreadPool()
|
||||
self._threadpool.start()
|
||||
self.getThreadPool = lambda: self._threadpool
|
||||
|
||||
self._threadCalls = Queue()
|
||||
|
||||
def callFromThread(
|
||||
self, callable: Callable[_P, object], *args: _P.args, **kwargs: _P.kwargs
|
||||
) -> None:
|
||||
self._threadCalls.put((callable, args, kwargs))
|
||||
|
||||
def _runThreadCalls(self):
|
||||
f, args, kwargs = self._threadCalls.get()
|
||||
f(*args, **kwargs)
|
||||
|
||||
def _stop(self):
|
||||
self._threadpool.stop()
|
||||
|
||||
def getDelayedCalls(self):
|
||||
# IReactorTime.getDelayedCalls
|
||||
pass
|
||||
|
||||
def seconds(self) -> float: # type: ignore[empty-body]
|
||||
# IReactorTime.seconds
|
||||
pass
|
||||
|
||||
def callInThread(
|
||||
self, callable: Callable[_P, object], *args: _P.args, **kwargs: _P.kwargs
|
||||
) -> None:
|
||||
# IReactorInThreads.callInThread
|
||||
pass
|
||||
|
||||
def suggestThreadPoolSize(self, size):
|
||||
# IReactorThreads.suggestThreadPoolSize
|
||||
pass
|
||||
|
||||
|
||||
class ThreadedResolverTests(TestCase):
|
||||
"""
|
||||
Tests for L{ThreadedResolver}.
|
||||
"""
|
||||
|
||||
def test_success(self):
|
||||
"""
|
||||
L{ThreadedResolver.getHostByName} returns a L{Deferred} which fires
|
||||
with the value returned by the call to L{socket.gethostbyname} in the
|
||||
threadpool of the reactor passed to L{ThreadedResolver.__init__}.
|
||||
"""
|
||||
ip = "10.0.0.17"
|
||||
name = "foo.bar.example.com"
|
||||
timeout = 30
|
||||
|
||||
reactor = FakeReactor()
|
||||
self.addCleanup(reactor._stop)
|
||||
|
||||
lookedUp = []
|
||||
resolvedTo = []
|
||||
|
||||
def fakeGetHostByName(name):
|
||||
lookedUp.append(name)
|
||||
return ip
|
||||
|
||||
self.patch(socket, "gethostbyname", fakeGetHostByName)
|
||||
|
||||
resolver = ThreadedResolver(reactor)
|
||||
d = resolver.getHostByName(name, (timeout,))
|
||||
d.addCallback(resolvedTo.append)
|
||||
|
||||
reactor._runThreadCalls()
|
||||
|
||||
self.assertEqual(lookedUp, [name])
|
||||
self.assertEqual(resolvedTo, [ip])
|
||||
|
||||
# Make sure that any timeout-related stuff gets cleaned up.
|
||||
reactor._clock.advance(timeout + 1)
|
||||
self.assertEqual(reactor._clock.calls, [])
|
||||
|
||||
def test_failure(self):
|
||||
"""
|
||||
L{ThreadedResolver.getHostByName} returns a L{Deferred} which fires a
|
||||
L{Failure} if the call to L{socket.gethostbyname} raises an exception.
|
||||
"""
|
||||
timeout = 30
|
||||
|
||||
reactor = FakeReactor()
|
||||
self.addCleanup(reactor._stop)
|
||||
|
||||
def fakeGetHostByName(name):
|
||||
raise OSError("ENOBUFS (this is a funny joke)")
|
||||
|
||||
self.patch(socket, "gethostbyname", fakeGetHostByName)
|
||||
|
||||
failedWith = []
|
||||
resolver = ThreadedResolver(reactor)
|
||||
d = resolver.getHostByName("some.name", (timeout,))
|
||||
self.assertFailure(d, DNSLookupError)
|
||||
d.addCallback(failedWith.append)
|
||||
|
||||
reactor._runThreadCalls()
|
||||
|
||||
self.assertEqual(len(failedWith), 1)
|
||||
|
||||
# Make sure that any timeout-related stuff gets cleaned up.
|
||||
reactor._clock.advance(timeout + 1)
|
||||
self.assertEqual(reactor._clock.calls, [])
|
||||
|
||||
def test_timeout(self):
|
||||
"""
|
||||
If L{socket.gethostbyname} does not complete before the specified
|
||||
timeout elapsed, the L{Deferred} returned by
|
||||
L{ThreadedResolver.getHostByName} fails with L{DNSLookupError}.
|
||||
"""
|
||||
timeout = 10
|
||||
|
||||
reactor = FakeReactor()
|
||||
self.addCleanup(reactor._stop)
|
||||
|
||||
result = Queue()
|
||||
|
||||
def fakeGetHostByName(name):
|
||||
raise result.get()
|
||||
|
||||
self.patch(socket, "gethostbyname", fakeGetHostByName)
|
||||
|
||||
failedWith = []
|
||||
resolver = ThreadedResolver(reactor)
|
||||
d = resolver.getHostByName("some.name", (timeout,))
|
||||
self.assertFailure(d, DNSLookupError)
|
||||
d.addCallback(failedWith.append)
|
||||
|
||||
reactor._clock.advance(timeout - 1)
|
||||
self.assertEqual(failedWith, [])
|
||||
reactor._clock.advance(1)
|
||||
self.assertEqual(len(failedWith), 1)
|
||||
|
||||
# Eventually the socket.gethostbyname does finish - in this case, with
|
||||
# an exception. Nobody cares, though.
|
||||
result.put(IOError("The I/O was errorful"))
|
||||
|
||||
def test_resolverGivenStr(self):
|
||||
"""
|
||||
L{ThreadedResolver.getHostByName} is passed L{str}, encoded using IDNA
|
||||
if required.
|
||||
"""
|
||||
calls = []
|
||||
|
||||
@implementer(IResolverSimple)
|
||||
class FakeResolver:
|
||||
def getHostByName(self, name, timeouts=()):
|
||||
calls.append(name)
|
||||
return Deferred()
|
||||
|
||||
class JustEnoughReactor(ReactorBase):
|
||||
def installWaker(self):
|
||||
pass
|
||||
|
||||
fake = FakeResolver()
|
||||
reactor = JustEnoughReactor()
|
||||
reactor.installResolver(fake)
|
||||
rec = FirstOneWins(Deferred())
|
||||
reactor.nameResolver.resolveHostName(rec, "example.example")
|
||||
reactor.nameResolver.resolveHostName(rec, "example.example")
|
||||
reactor.nameResolver.resolveHostName(rec, "v\xe4\xe4ntynyt.example")
|
||||
reactor.nameResolver.resolveHostName(rec, "\u0440\u0444.example")
|
||||
reactor.nameResolver.resolveHostName(rec, "xn----7sbb4ac0ad0be6cf.xn--p1ai")
|
||||
|
||||
self.assertEqual(len(calls), 5)
|
||||
self.assertEqual(list(map(type, calls)), [str] * 5)
|
||||
self.assertEqual("example.example", calls[0])
|
||||
self.assertEqual("example.example", calls[1])
|
||||
self.assertEqual("xn--vntynyt-5waa.example", calls[2])
|
||||
self.assertEqual("xn--p1ai.example", calls[3])
|
||||
self.assertEqual("xn----7sbb4ac0ad0be6cf.xn--p1ai", calls[4])
|
||||
|
||||
|
||||
def nothing():
|
||||
"""
|
||||
Function used by L{DelayedCallTests.test_str}.
|
||||
"""
|
||||
|
||||
|
||||
class DelayedCallMixin:
|
||||
"""
|
||||
L{DelayedCall}
|
||||
"""
|
||||
|
||||
def _getDelayedCallAt(self, time):
|
||||
"""
|
||||
Get a L{DelayedCall} instance at a given C{time}.
|
||||
|
||||
@param time: The absolute time at which the returned L{DelayedCall}
|
||||
will be scheduled.
|
||||
"""
|
||||
|
||||
def noop(call):
|
||||
pass
|
||||
|
||||
return DelayedCall(time, lambda: None, (), {}, noop, noop, None)
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Create two L{DelayedCall} instanced scheduled to run at different
|
||||
times.
|
||||
"""
|
||||
self.zero = self._getDelayedCallAt(0)
|
||||
self.one = self._getDelayedCallAt(1)
|
||||
|
||||
def test_str(self):
|
||||
"""
|
||||
The string representation of a L{DelayedCall} instance, as returned by
|
||||
L{str}, includes the unsigned id of the instance, as well as its state,
|
||||
the function to be called, and the function arguments.
|
||||
"""
|
||||
dc = DelayedCall(12, nothing, (3,), {"A": 5}, None, None, lambda: 1.5)
|
||||
self.assertEqual(
|
||||
str(dc),
|
||||
"<DelayedCall 0x%x [10.5s] called=0 cancelled=0 nothing(3, A=5)>"
|
||||
% (id(dc),),
|
||||
)
|
||||
|
||||
def test_repr(self):
|
||||
"""
|
||||
The string representation of a L{DelayedCall} instance, as returned by
|
||||
{repr}, is identical to that returned by L{str}.
|
||||
"""
|
||||
dc = DelayedCall(13, nothing, (6,), {"A": 9}, None, None, lambda: 1.6)
|
||||
self.assertEqual(str(dc), repr(dc))
|
||||
|
||||
def test_lt(self):
|
||||
"""
|
||||
For two instances of L{DelayedCall} C{a} and C{b}, C{a < b} is true
|
||||
if and only if C{a} is scheduled to run before C{b}.
|
||||
"""
|
||||
zero, one = self.zero, self.one
|
||||
self.assertTrue(zero < one)
|
||||
self.assertFalse(one < zero)
|
||||
self.assertFalse(zero < zero)
|
||||
self.assertFalse(one < one)
|
||||
|
||||
def test_le(self):
|
||||
"""
|
||||
For two instances of L{DelayedCall} C{a} and C{b}, C{a <= b} is true
|
||||
if and only if C{a} is scheduled to run before C{b} or at the same
|
||||
time as C{b}.
|
||||
"""
|
||||
zero, one = self.zero, self.one
|
||||
self.assertTrue(zero <= one)
|
||||
self.assertFalse(one <= zero)
|
||||
self.assertTrue(zero <= zero)
|
||||
self.assertTrue(one <= one)
|
||||
|
||||
def test_gt(self):
|
||||
"""
|
||||
For two instances of L{DelayedCall} C{a} and C{b}, C{a > b} is true
|
||||
if and only if C{a} is scheduled to run after C{b}.
|
||||
"""
|
||||
zero, one = self.zero, self.one
|
||||
self.assertTrue(one > zero)
|
||||
self.assertFalse(zero > one)
|
||||
self.assertFalse(zero > zero)
|
||||
self.assertFalse(one > one)
|
||||
|
||||
def test_ge(self):
|
||||
"""
|
||||
For two instances of L{DelayedCall} C{a} and C{b}, C{a > b} is true
|
||||
if and only if C{a} is scheduled to run after C{b} or at the same
|
||||
time as C{b}.
|
||||
"""
|
||||
zero, one = self.zero, self.one
|
||||
self.assertTrue(one >= zero)
|
||||
self.assertFalse(zero >= one)
|
||||
self.assertTrue(zero >= zero)
|
||||
self.assertTrue(one >= one)
|
||||
|
||||
def test_eq(self):
|
||||
"""
|
||||
A L{DelayedCall} instance is only equal to itself.
|
||||
"""
|
||||
# Explicitly use == here, instead of assertEqual, to be more
|
||||
# confident __eq__ is being tested.
|
||||
self.assertFalse(self.zero == self.one)
|
||||
self.assertTrue(self.zero == self.zero)
|
||||
self.assertTrue(self.one == self.one)
|
||||
|
||||
def test_ne(self):
|
||||
"""
|
||||
A L{DelayedCall} instance is not equal to any other object.
|
||||
"""
|
||||
# Explicitly use != here, instead of assertEqual, to be more
|
||||
# confident __ne__ is being tested.
|
||||
self.assertTrue(self.zero != self.one)
|
||||
self.assertFalse(self.zero != self.zero)
|
||||
self.assertFalse(self.one != self.one)
|
||||
|
||||
|
||||
class DelayedCallNoDebugTests(DelayedCallMixin, TestCase):
|
||||
"""
|
||||
L{DelayedCall}
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Turn debug off.
|
||||
"""
|
||||
self.patch(DelayedCall, "debug", False)
|
||||
DelayedCallMixin.setUp(self)
|
||||
|
||||
def test_str(self):
|
||||
"""
|
||||
The string representation of a L{DelayedCall} instance, as returned by
|
||||
L{str}, includes the unsigned id of the instance, as well as its state,
|
||||
the function to be called, and the function arguments.
|
||||
"""
|
||||
dc = DelayedCall(12, nothing, (3,), {"A": 5}, None, None, lambda: 1.5)
|
||||
expected = (
|
||||
"<DelayedCall 0x{:x} [10.5s] called=0 cancelled=0 "
|
||||
"nothing(3, A=5)>".format(id(dc))
|
||||
)
|
||||
self.assertEqual(str(dc), expected)
|
||||
|
||||
def test_switchToDebug(self):
|
||||
"""
|
||||
If L{DelayedCall.debug} changes from C{0} to C{1} between
|
||||
L{DelayeCall.__init__} and L{DelayedCall.__repr__} then
|
||||
L{DelayedCall.__repr__} returns a string that does not include the
|
||||
creator stack.
|
||||
"""
|
||||
dc = DelayedCall(3, lambda: None, (), {}, nothing, nothing, lambda: 2)
|
||||
dc.debug = 1
|
||||
self.assertNotIn("traceback at creation", repr(dc))
|
||||
|
||||
|
||||
class DelayedCallDebugTests(DelayedCallMixin, TestCase):
|
||||
"""
|
||||
L{DelayedCall}
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Turn debug on.
|
||||
"""
|
||||
self.patch(DelayedCall, "debug", True)
|
||||
DelayedCallMixin.setUp(self)
|
||||
|
||||
def test_str(self):
|
||||
"""
|
||||
The string representation of a L{DelayedCall} instance, as returned by
|
||||
L{str}, includes the unsigned id of the instance, as well as its state,
|
||||
the function to be called, and the function arguments.
|
||||
"""
|
||||
dc = DelayedCall(12, nothing, (3,), {"A": 5}, None, None, lambda: 1.5)
|
||||
expectedRegexp = (
|
||||
"<DelayedCall 0x{:x} \\[10.5s\\] called=0 cancelled=0 "
|
||||
"nothing\\(3, A=5\\)\n\n"
|
||||
"traceback at creation:".format(id(dc))
|
||||
)
|
||||
self.assertRegex(str(dc), expectedRegexp)
|
||||
|
||||
def test_switchFromDebug(self):
|
||||
"""
|
||||
If L{DelayedCall.debug} changes from C{1} to C{0} between
|
||||
L{DelayeCall.__init__} and L{DelayedCall.__repr__} then
|
||||
L{DelayedCall.__repr__} returns a string that includes the creator
|
||||
stack (we captured it, we might as well display it).
|
||||
"""
|
||||
dc = DelayedCall(3, lambda: None, (), {}, nothing, nothing, lambda: 2)
|
||||
dc.debug = 0
|
||||
self.assertIn("traceback at creation", repr(dc))
|
||||
|
||||
|
||||
class TestSpySignalCapturingReactor(ReactorBase):
|
||||
|
||||
"""
|
||||
Subclass of ReactorBase to capture signals delivered to the
|
||||
reactor for inspection.
|
||||
"""
|
||||
|
||||
def installWaker(self):
|
||||
"""
|
||||
Required method, unused.
|
||||
"""
|
||||
|
||||
|
||||
@skipIf(not signal, "signal module not available")
|
||||
class ReactorBaseSignalTests(TestCase):
|
||||
|
||||
"""
|
||||
Tests to exercise ReactorBase's signal exit reporting path.
|
||||
"""
|
||||
|
||||
def test_exitSignalDefaultsToNone(self):
|
||||
"""
|
||||
The default value of the _exitSignal attribute is None.
|
||||
"""
|
||||
reactor = TestSpySignalCapturingReactor()
|
||||
self.assertIs(None, reactor._exitSignal)
|
||||
|
||||
def test_captureSIGINT(self):
|
||||
"""
|
||||
ReactorBase's SIGINT handler saves the value of SIGINT to the
|
||||
_exitSignal attribute.
|
||||
"""
|
||||
reactor = TestSpySignalCapturingReactor()
|
||||
reactor.sigInt(signal.SIGINT, None)
|
||||
self.assertEquals(signal.SIGINT, reactor._exitSignal)
|
||||
|
||||
def test_captureSIGTERM(self):
|
||||
"""
|
||||
ReactorBase's SIGTERM handler saves the value of SIGTERM to the
|
||||
_exitSignal attribute.
|
||||
"""
|
||||
reactor = TestSpySignalCapturingReactor()
|
||||
reactor.sigTerm(signal.SIGTERM, None)
|
||||
self.assertEquals(signal.SIGTERM, reactor._exitSignal)
|
||||
|
||||
def test_captureSIGBREAK(self):
|
||||
"""
|
||||
ReactorBase's SIGBREAK handler saves the value of SIGBREAK to the
|
||||
_exitSignal attribute.
|
||||
"""
|
||||
if not hasattr(signal, "SIGBREAK"):
|
||||
raise SkipTest("signal module does not have SIGBREAK")
|
||||
|
||||
reactor = TestSpySignalCapturingReactor()
|
||||
reactor.sigBreak(signal.SIGBREAK, None)
|
||||
self.assertEquals(signal.SIGBREAK, reactor._exitSignal)
|
||||
@@ -0,0 +1,75 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.internet._baseprocess} which implements process-related
|
||||
functionality that is useful in all platforms supporting L{IReactorProcess}.
|
||||
"""
|
||||
|
||||
from twisted.internet._baseprocess import BaseProcess
|
||||
from twisted.python.deprecate import getWarningMethod, setWarningMethod
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class BaseProcessTests(TestCase):
|
||||
"""
|
||||
Tests for L{BaseProcess}, a parent class for other classes which represent
|
||||
processes which implements functionality common to many different process
|
||||
implementations.
|
||||
"""
|
||||
|
||||
def test_callProcessExited(self):
|
||||
"""
|
||||
L{BaseProcess._callProcessExited} calls the C{processExited} method of
|
||||
its C{proto} attribute and passes it a L{Failure} wrapping the given
|
||||
exception.
|
||||
"""
|
||||
|
||||
class FakeProto:
|
||||
reason = None
|
||||
|
||||
def processExited(self, reason):
|
||||
self.reason = reason
|
||||
|
||||
reason = RuntimeError("fake reason")
|
||||
process = BaseProcess(FakeProto())
|
||||
process._callProcessExited(reason)
|
||||
process.proto.reason.trap(RuntimeError)
|
||||
self.assertIs(reason, process.proto.reason.value)
|
||||
|
||||
def test_callProcessExitedMissing(self):
|
||||
"""
|
||||
L{BaseProcess._callProcessExited} emits a L{DeprecationWarning} if the
|
||||
object referred to by its C{proto} attribute has no C{processExited}
|
||||
method.
|
||||
"""
|
||||
|
||||
class FakeProto:
|
||||
pass
|
||||
|
||||
reason = object()
|
||||
process = BaseProcess(FakeProto())
|
||||
|
||||
self.addCleanup(setWarningMethod, getWarningMethod())
|
||||
warnings = []
|
||||
|
||||
def collect(message, category, stacklevel):
|
||||
warnings.append((message, category, stacklevel))
|
||||
|
||||
setWarningMethod(collect)
|
||||
|
||||
process._callProcessExited(reason)
|
||||
|
||||
[(message, category, stacklevel)] = warnings
|
||||
self.assertEqual(
|
||||
message,
|
||||
"Since Twisted 8.2, IProcessProtocol.processExited is required. "
|
||||
"%s.%s must implement it." % (FakeProto.__module__, FakeProto.__name__),
|
||||
)
|
||||
self.assertIs(category, DeprecationWarning)
|
||||
# The stacklevel doesn't really make sense for this kind of
|
||||
# deprecation. Requiring it to be 0 will at least avoid pointing to
|
||||
# any part of Twisted or a random part of the application's code, which
|
||||
# I think would be more misleading than having it point inside the
|
||||
# warning system itself. -exarkun
|
||||
self.assertEqual(stacklevel, 0)
|
||||
@@ -0,0 +1,101 @@
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
from .reactormixins import ReactorBuilder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
fakeBase = SynchronousTestCase
|
||||
else:
|
||||
fakeBase = object
|
||||
|
||||
|
||||
def noop() -> None:
|
||||
"""
|
||||
Do-nothing callable. Stub for testing.
|
||||
"""
|
||||
|
||||
|
||||
noop() # Exercise for coverage, since it will never be called below.
|
||||
|
||||
|
||||
class CoreFoundationSpecificTests(ReactorBuilder, fakeBase):
|
||||
"""
|
||||
Tests for platform interactions of the CoreFoundation-based reactor.
|
||||
"""
|
||||
|
||||
_reactors = ["twisted.internet.cfreactor.CFReactor"]
|
||||
|
||||
def test_whiteboxStopSimulating(self) -> None:
|
||||
"""
|
||||
CFReactor's simulation timer is None after CFReactor crashes.
|
||||
"""
|
||||
r = self.buildReactor()
|
||||
r.callLater(0, r.crash)
|
||||
r.callLater(100, noop)
|
||||
self.runReactor(r)
|
||||
self.assertIs(r._currentSimulator, None)
|
||||
|
||||
def test_callLaterLeakage(self) -> None:
|
||||
"""
|
||||
callLater should not leak global state into CoreFoundation which will
|
||||
be invoked by a different reactor running the main loop.
|
||||
|
||||
@note: this test may actually be usable for other reactors as well, so
|
||||
we may wish to promote it to ensure this invariant across other
|
||||
foreign-main-loop reactors.
|
||||
"""
|
||||
r = self.buildReactor()
|
||||
delayed = r.callLater(0, noop)
|
||||
r2 = self.buildReactor()
|
||||
|
||||
def stopBlocking() -> None:
|
||||
r2.callLater(0, r2stop)
|
||||
|
||||
def r2stop() -> None:
|
||||
r2.stop()
|
||||
|
||||
r2.callLater(0, stopBlocking)
|
||||
self.runReactor(r2)
|
||||
self.assertEqual(r.getDelayedCalls(), [delayed])
|
||||
|
||||
def test_whiteboxIterate(self) -> None:
|
||||
"""
|
||||
C{.iterate()} should remove the CFTimer that will run Twisted's
|
||||
callLaters from the loop, even if one is still pending. We test this
|
||||
state indirectly with a white-box assertion by verifying the
|
||||
C{_currentSimulator} is set to C{None}, since CoreFoundation does not
|
||||
allow us to enumerate all active timers or sources.
|
||||
"""
|
||||
r = self.buildReactor()
|
||||
x: List[int] = []
|
||||
r.callLater(0, x.append, 1)
|
||||
delayed = r.callLater(100, noop)
|
||||
r.iterate()
|
||||
self.assertIs(r._currentSimulator, None)
|
||||
self.assertEqual(r.getDelayedCalls(), [delayed])
|
||||
self.assertEqual(x, [1])
|
||||
|
||||
def test_noTimers(self) -> None:
|
||||
"""
|
||||
The loop can wake up just fine even if there are no timers in it.
|
||||
"""
|
||||
r = self.buildReactor()
|
||||
stopped = []
|
||||
|
||||
def doStop() -> None:
|
||||
r.stop()
|
||||
stopped.append("yes")
|
||||
|
||||
def sleepThenStop() -> None:
|
||||
r.callFromThread(doStop)
|
||||
|
||||
r.callLater(0, r.callInThread, sleepThenStop)
|
||||
# Can't use runReactor here because it does a callLater. This is
|
||||
# therefore a somewhat risky test: inherently, this is the "no timed
|
||||
# events anywhere in the reactor" test case and so we can't have a
|
||||
# timeout for it.
|
||||
r.run()
|
||||
self.assertEqual(stopped, ["yes"])
|
||||
|
||||
|
||||
globals().update(CoreFoundationSpecificTests.makeTestCaseClasses())
|
||||
@@ -0,0 +1,316 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for implementations of L{IReactorCore}.
|
||||
"""
|
||||
|
||||
|
||||
import signal
|
||||
import time
|
||||
from types import FrameType
|
||||
from typing import Callable, List, Optional, Tuple, Union, cast
|
||||
|
||||
from twisted.internet.abstract import FileDescriptor
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.internet.error import ReactorAlreadyRunning, ReactorNotRestartable
|
||||
from twisted.internet.test.reactormixins import ReactorBuilder
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
|
||||
|
||||
class SystemEventTestsBuilder(ReactorBuilder):
|
||||
"""
|
||||
Builder defining tests relating to L{IReactorCore.addSystemEventTrigger}
|
||||
and L{IReactorCore.fireSystemEvent}.
|
||||
"""
|
||||
|
||||
def test_stopWhenNotStarted(self) -> None:
|
||||
"""
|
||||
C{reactor.stop()} raises L{RuntimeError} when called when the reactor
|
||||
has not been started.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
cast(SynchronousTestCase, self).assertRaises(RuntimeError, reactor.stop)
|
||||
|
||||
def test_stopWhenAlreadyStopped(self) -> None:
|
||||
"""
|
||||
C{reactor.stop()} raises L{RuntimeError} when called after the reactor
|
||||
has been stopped.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
reactor.callWhenRunning(reactor.stop)
|
||||
self.runReactor(reactor)
|
||||
cast(SynchronousTestCase, self).assertRaises(RuntimeError, reactor.stop)
|
||||
|
||||
def test_callWhenRunningOrder(self) -> None:
|
||||
"""
|
||||
Functions are run in the order that they were passed to
|
||||
L{reactor.callWhenRunning}.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
events: List[str] = []
|
||||
reactor.callWhenRunning(events.append, "first")
|
||||
reactor.callWhenRunning(events.append, "second")
|
||||
reactor.callWhenRunning(reactor.stop)
|
||||
self.runReactor(reactor)
|
||||
cast(SynchronousTestCase, self).assertEqual(events, ["first", "second"])
|
||||
|
||||
def test_runningForStartupEvents(self) -> None:
|
||||
"""
|
||||
The reactor is not running when C{"before"} C{"startup"} triggers are
|
||||
called and is running when C{"during"} and C{"after"} C{"startup"}
|
||||
triggers are called.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
state = {}
|
||||
|
||||
def beforeStartup() -> None:
|
||||
state["before"] = reactor.running
|
||||
|
||||
def duringStartup() -> None:
|
||||
state["during"] = reactor.running
|
||||
|
||||
def afterStartup() -> None:
|
||||
state["after"] = reactor.running
|
||||
|
||||
testCase = cast(SynchronousTestCase, self)
|
||||
|
||||
reactor.addSystemEventTrigger("before", "startup", beforeStartup)
|
||||
reactor.addSystemEventTrigger("during", "startup", duringStartup)
|
||||
reactor.addSystemEventTrigger("after", "startup", afterStartup)
|
||||
reactor.callWhenRunning(reactor.stop)
|
||||
testCase.assertEqual(state, {})
|
||||
self.runReactor(reactor)
|
||||
testCase.assertEqual(state, {"before": False, "during": True, "after": True})
|
||||
|
||||
def test_signalHandlersInstalledDuringStartup(self) -> None:
|
||||
"""
|
||||
Signal handlers are installed in responsed to the C{"during"}
|
||||
C{"startup"}.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
phase: Optional[str] = None
|
||||
|
||||
def beforeStartup() -> None:
|
||||
nonlocal phase
|
||||
phase = "before"
|
||||
|
||||
def afterStartup() -> None:
|
||||
nonlocal phase
|
||||
phase = "after"
|
||||
|
||||
reactor.addSystemEventTrigger("before", "startup", beforeStartup)
|
||||
reactor.addSystemEventTrigger("after", "startup", afterStartup)
|
||||
|
||||
sawPhase = []
|
||||
|
||||
def fakeSignal(signum: int, action: Callable[[int, FrameType], None]) -> None:
|
||||
sawPhase.append(phase)
|
||||
|
||||
testCase = cast(SynchronousTestCase, self)
|
||||
|
||||
testCase.patch(signal, "signal", fakeSignal)
|
||||
reactor.callWhenRunning(reactor.stop)
|
||||
testCase.assertIsNone(phase)
|
||||
testCase.assertEqual(sawPhase, [])
|
||||
self.runReactor(reactor)
|
||||
testCase.assertIn("before", sawPhase)
|
||||
testCase.assertEqual(phase, "after")
|
||||
|
||||
def test_stopShutDownEvents(self) -> None:
|
||||
"""
|
||||
C{reactor.stop()} fires all three phases of shutdown event triggers
|
||||
before it makes C{reactor.run()} return.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
events = []
|
||||
reactor.addSystemEventTrigger(
|
||||
"before", "shutdown", lambda: events.append(("before", "shutdown"))
|
||||
)
|
||||
reactor.addSystemEventTrigger(
|
||||
"during", "shutdown", lambda: events.append(("during", "shutdown"))
|
||||
)
|
||||
reactor.addSystemEventTrigger(
|
||||
"after", "shutdown", lambda: events.append(("after", "shutdown"))
|
||||
)
|
||||
reactor.callWhenRunning(reactor.stop)
|
||||
self.runReactor(reactor)
|
||||
cast(SynchronousTestCase, self).assertEqual(
|
||||
events,
|
||||
[("before", "shutdown"), ("during", "shutdown"), ("after", "shutdown")],
|
||||
)
|
||||
|
||||
def test_shutdownFiresTriggersAsynchronously(self) -> None:
|
||||
"""
|
||||
C{"before"} C{"shutdown"} triggers are not run synchronously from
|
||||
L{reactor.stop}.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
events: List[str] = []
|
||||
reactor.addSystemEventTrigger(
|
||||
"before", "shutdown", events.append, "before shutdown"
|
||||
)
|
||||
|
||||
def stopIt() -> None:
|
||||
reactor.stop()
|
||||
events.append("stopped")
|
||||
|
||||
testCase = cast(SynchronousTestCase, self)
|
||||
|
||||
reactor.callWhenRunning(stopIt)
|
||||
testCase.assertEqual(events, [])
|
||||
self.runReactor(reactor)
|
||||
testCase.assertEqual(events, ["stopped", "before shutdown"])
|
||||
|
||||
def test_shutdownDisconnectsCleanly(self) -> None:
|
||||
"""
|
||||
A L{IFileDescriptor.connectionLost} implementation which raises an
|
||||
exception does not prevent the remaining L{IFileDescriptor}s from
|
||||
having their C{connectionLost} method called.
|
||||
"""
|
||||
lostOK = [False]
|
||||
|
||||
# Subclass FileDescriptor to get logPrefix
|
||||
class ProblematicFileDescriptor(FileDescriptor):
|
||||
def connectionLost(self, reason: Failure) -> None:
|
||||
raise RuntimeError("simulated connectionLost error")
|
||||
|
||||
class OKFileDescriptor(FileDescriptor):
|
||||
def connectionLost(self, reason: Failure) -> None:
|
||||
lostOK[0] = True
|
||||
|
||||
testCase = cast(SynchronousTestCase, self)
|
||||
reactor = self.buildReactor()
|
||||
|
||||
# Unfortunately, it is necessary to patch removeAll to directly control
|
||||
# the order of the returned values. The test is only valid if
|
||||
# ProblematicFileDescriptor comes first. Also, return these
|
||||
# descriptors only the first time removeAll is called so that if it is
|
||||
# called again the file descriptors aren't re-disconnected.
|
||||
fds = iter([ProblematicFileDescriptor(), OKFileDescriptor()])
|
||||
reactor.removeAll = lambda: fds
|
||||
reactor.callWhenRunning(reactor.stop)
|
||||
self.runReactor(reactor)
|
||||
testCase.assertEqual(len(testCase.flushLoggedErrors(RuntimeError)), 1)
|
||||
testCase.assertTrue(lostOK[0])
|
||||
|
||||
def test_multipleRun(self) -> None:
|
||||
"""
|
||||
C{reactor.run()} raises L{ReactorAlreadyRunning} when called when
|
||||
the reactor is already running.
|
||||
"""
|
||||
events: List[str] = []
|
||||
|
||||
testCase = cast(SynchronousTestCase, self)
|
||||
|
||||
def reentrantRun() -> None:
|
||||
testCase.assertRaises(ReactorAlreadyRunning, reactor.run)
|
||||
events.append("tested")
|
||||
|
||||
reactor = self.buildReactor()
|
||||
reactor.callWhenRunning(reentrantRun)
|
||||
reactor.callWhenRunning(reactor.stop)
|
||||
self.runReactor(reactor)
|
||||
testCase.assertEqual(events, ["tested"])
|
||||
|
||||
def test_runWithAsynchronousBeforeStartupTrigger(self) -> None:
|
||||
"""
|
||||
When there is a C{'before'} C{'startup'} trigger which returns an
|
||||
unfired L{Deferred}, C{reactor.run()} starts the reactor and does not
|
||||
return until after C{reactor.stop()} is called
|
||||
"""
|
||||
events = []
|
||||
|
||||
def trigger() -> Deferred[object]:
|
||||
events.append("trigger")
|
||||
d: Deferred[object] = Deferred()
|
||||
d.addCallback(callback)
|
||||
reactor.callLater(0, d.callback, None)
|
||||
return d
|
||||
|
||||
def callback(ignored: object) -> None:
|
||||
events.append("callback")
|
||||
reactor.stop()
|
||||
|
||||
reactor = self.buildReactor()
|
||||
reactor.addSystemEventTrigger("before", "startup", trigger)
|
||||
self.runReactor(reactor)
|
||||
cast(SynchronousTestCase, self).assertEqual(events, ["trigger", "callback"])
|
||||
|
||||
def test_iterate(self) -> None:
|
||||
"""
|
||||
C{reactor.iterate()} does not block.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
t = reactor.callLater(5, reactor.crash)
|
||||
|
||||
start = time.time()
|
||||
reactor.iterate(0) # Shouldn't block
|
||||
elapsed = time.time() - start
|
||||
|
||||
cast(SynchronousTestCase, self).assertTrue(elapsed < 2)
|
||||
t.cancel()
|
||||
|
||||
def test_crash(self) -> None:
|
||||
"""
|
||||
C{reactor.crash()} stops the reactor and does not fire shutdown
|
||||
triggers.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
events = []
|
||||
reactor.addSystemEventTrigger(
|
||||
"before", "shutdown", lambda: events.append(("before", "shutdown"))
|
||||
)
|
||||
reactor.callWhenRunning(reactor.callLater, 0, reactor.crash)
|
||||
self.runReactor(reactor)
|
||||
testCase = cast(SynchronousTestCase, self)
|
||||
testCase.assertFalse(reactor.running)
|
||||
testCase.assertFalse(
|
||||
events, "Shutdown triggers invoked but they should not have been."
|
||||
)
|
||||
|
||||
def test_runAfterCrash(self) -> None:
|
||||
"""
|
||||
C{reactor.run()} restarts the reactor after it has been stopped by
|
||||
C{reactor.crash()}.
|
||||
"""
|
||||
events: List[Union[str, Tuple[str, bool]]] = []
|
||||
|
||||
def crash() -> None:
|
||||
events.append("crash")
|
||||
reactor.crash()
|
||||
|
||||
reactor = self.buildReactor()
|
||||
reactor.callWhenRunning(crash)
|
||||
self.runReactor(reactor)
|
||||
|
||||
def stop() -> None:
|
||||
events.append(("stop", reactor.running))
|
||||
reactor.stop()
|
||||
|
||||
reactor.callWhenRunning(stop)
|
||||
self.runReactor(reactor)
|
||||
cast(SynchronousTestCase, self).assertEqual(events, ["crash", ("stop", True)])
|
||||
|
||||
def test_runAfterStop(self) -> None:
|
||||
"""
|
||||
C{reactor.run()} raises L{ReactorNotRestartable} when called when
|
||||
the reactor is being run after getting stopped priorly.
|
||||
"""
|
||||
events: List[str] = []
|
||||
|
||||
testCase = cast(SynchronousTestCase, self)
|
||||
|
||||
def restart() -> None:
|
||||
testCase.assertRaises(ReactorNotRestartable, reactor.run)
|
||||
events.append("tested")
|
||||
|
||||
reactor = self.buildReactor()
|
||||
reactor.callWhenRunning(reactor.stop)
|
||||
reactor.addSystemEventTrigger("after", "shutdown", restart)
|
||||
self.runReactor(reactor)
|
||||
testCase.assertEqual(events, ["tested"])
|
||||
|
||||
|
||||
globals().update(SystemEventTestsBuilder.makeTestCaseClasses())
|
||||
@@ -0,0 +1,114 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.internet.default}.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import select
|
||||
import sys
|
||||
from typing import Callable
|
||||
|
||||
from twisted.internet import default
|
||||
from twisted.internet.default import _getInstallFunction, install
|
||||
from twisted.internet.interfaces import IReactorCore
|
||||
from twisted.internet.test.test_main import NoReactor
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.python.runtime import Platform
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
|
||||
unix = Platform("posix", "other")
|
||||
linux = Platform("posix", "linux2")
|
||||
windows = Platform("nt", "win32")
|
||||
osx = Platform("posix", "darwin")
|
||||
|
||||
|
||||
class PollReactorTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for the cases of L{twisted.internet.default._getInstallFunction}
|
||||
in which it picks the poll(2) or epoll(7)-based reactors.
|
||||
"""
|
||||
|
||||
def assertIsPoll(self, install: Callable[..., object]) -> None:
|
||||
"""
|
||||
Assert the given function will install the poll() reactor, or select()
|
||||
if poll() is unavailable.
|
||||
"""
|
||||
if hasattr(select, "poll"):
|
||||
self.assertEqual(install.__module__, "twisted.internet.pollreactor")
|
||||
else:
|
||||
self.assertEqual(install.__module__, "twisted.internet.selectreactor")
|
||||
|
||||
def test_unix(self) -> None:
|
||||
"""
|
||||
L{_getInstallFunction} chooses the poll reactor on arbitrary Unix
|
||||
platforms, falling back to select(2) if it is unavailable.
|
||||
"""
|
||||
install = _getInstallFunction(unix)
|
||||
self.assertIsPoll(install)
|
||||
|
||||
def test_linux(self) -> None:
|
||||
"""
|
||||
L{_getInstallFunction} chooses the epoll reactor on Linux, or poll if
|
||||
epoll is unavailable.
|
||||
"""
|
||||
install = _getInstallFunction(linux)
|
||||
if requireModule("twisted.internet.epollreactor") is None:
|
||||
self.assertIsPoll(install)
|
||||
else:
|
||||
self.assertEqual(install.__module__, "twisted.internet.epollreactor")
|
||||
|
||||
|
||||
class SelectReactorTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for the cases of L{twisted.internet.default._getInstallFunction}
|
||||
in which it picks the select(2)-based reactor.
|
||||
"""
|
||||
|
||||
def test_osx(self) -> None:
|
||||
"""
|
||||
L{_getInstallFunction} chooses the select reactor on macOS.
|
||||
"""
|
||||
install = _getInstallFunction(osx)
|
||||
self.assertEqual(install.__module__, "twisted.internet.selectreactor")
|
||||
|
||||
def test_windows(self) -> None:
|
||||
"""
|
||||
L{_getInstallFunction} chooses the select reactor on Windows.
|
||||
"""
|
||||
install = _getInstallFunction(windows)
|
||||
self.assertEqual(install.__module__, "twisted.internet.selectreactor")
|
||||
|
||||
|
||||
class InstallationTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for actual installation of the reactor.
|
||||
"""
|
||||
|
||||
def test_install(self) -> None:
|
||||
"""
|
||||
L{install} installs a reactor.
|
||||
"""
|
||||
with NoReactor():
|
||||
install()
|
||||
self.assertIn("twisted.internet.reactor", sys.modules)
|
||||
|
||||
def test_reactor(self) -> None:
|
||||
"""
|
||||
Importing L{twisted.internet.reactor} installs the default reactor if
|
||||
none is installed.
|
||||
"""
|
||||
installed: list[bool] = []
|
||||
|
||||
def installer() -> object:
|
||||
installed.append(True)
|
||||
return install()
|
||||
|
||||
self.patch(default, "install", installer)
|
||||
|
||||
with NoReactor():
|
||||
from twisted.internet import reactor
|
||||
|
||||
self.assertTrue(IReactorCore.providedBy(reactor))
|
||||
self.assertEqual(installed, [True])
|
||||
@@ -0,0 +1,241 @@
|
||||
# -*- test-case-name: twisted.internet.test.test_defer_await -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for C{await} support in Deferreds.
|
||||
"""
|
||||
|
||||
import types
|
||||
|
||||
from typing_extensions import NoReturn
|
||||
|
||||
from twisted.internet.defer import (
|
||||
Deferred,
|
||||
ensureDeferred,
|
||||
fail,
|
||||
maybeDeferred,
|
||||
succeed,
|
||||
)
|
||||
from twisted.internet.task import Clock
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class SampleException(Exception):
|
||||
"""
|
||||
A specific sample exception for testing.
|
||||
"""
|
||||
|
||||
|
||||
class AwaitTests(TestCase):
|
||||
"""
|
||||
Tests for using Deferreds in conjunction with PEP-492.
|
||||
"""
|
||||
|
||||
def test_awaitReturnsIterable(self) -> None:
|
||||
"""
|
||||
C{Deferred.__await__} returns an iterable.
|
||||
"""
|
||||
d: Deferred[None] = Deferred()
|
||||
awaitedDeferred = d.__await__()
|
||||
self.assertEqual(awaitedDeferred, iter(awaitedDeferred))
|
||||
|
||||
def test_deferredFromCoroutine(self) -> None:
|
||||
"""
|
||||
L{Deferred.fromCoroutine} will turn a coroutine into a L{Deferred}.
|
||||
"""
|
||||
|
||||
async def run() -> str:
|
||||
d = succeed("bar")
|
||||
await d
|
||||
res = await run2()
|
||||
return res
|
||||
|
||||
async def run2() -> str:
|
||||
d = succeed("foo")
|
||||
res = await d
|
||||
return res
|
||||
|
||||
# It's a coroutine...
|
||||
r = run()
|
||||
self.assertIsInstance(r, types.CoroutineType)
|
||||
|
||||
# Now it's a Deferred.
|
||||
d = Deferred.fromCoroutine(r)
|
||||
self.assertIsInstance(d, Deferred)
|
||||
|
||||
# The Deferred has the result we want.
|
||||
res = self.successResultOf(d)
|
||||
self.assertEqual(res, "foo")
|
||||
|
||||
def test_basic(self) -> None:
|
||||
"""
|
||||
L{Deferred.fromCoroutine} allows a function to C{await} on a
|
||||
L{Deferred}.
|
||||
"""
|
||||
|
||||
async def run() -> str:
|
||||
d = succeed("foo")
|
||||
res = await d
|
||||
return res
|
||||
|
||||
d = Deferred.fromCoroutine(run())
|
||||
res = self.successResultOf(d)
|
||||
self.assertEqual(res, "foo")
|
||||
|
||||
def test_basicEnsureDeferred(self) -> None:
|
||||
"""
|
||||
L{ensureDeferred} allows a function to C{await} on a L{Deferred}.
|
||||
"""
|
||||
|
||||
async def run() -> str:
|
||||
d = succeed("foo")
|
||||
res = await d
|
||||
return res
|
||||
|
||||
d = ensureDeferred(run())
|
||||
res = self.successResultOf(d)
|
||||
self.assertEqual(res, "foo")
|
||||
|
||||
def test_exception(self) -> None:
|
||||
"""
|
||||
An exception in a coroutine scheduled with L{Deferred.fromCoroutine}
|
||||
will cause the returned L{Deferred} to fire with a failure.
|
||||
"""
|
||||
|
||||
async def run() -> NoReturn:
|
||||
d = succeed("foo")
|
||||
await d
|
||||
raise ValueError("Oh no!")
|
||||
|
||||
d = Deferred.fromCoroutine(run())
|
||||
res = self.failureResultOf(d)
|
||||
self.assertEqual(type(res.value), ValueError)
|
||||
self.assertEqual(res.value.args, ("Oh no!",))
|
||||
|
||||
def test_synchronousDeferredFailureTraceback(self) -> None:
|
||||
"""
|
||||
When a Deferred is awaited upon that has already failed with a Failure
|
||||
that has a traceback, both the place that the synchronous traceback
|
||||
comes from and the awaiting line are shown in the traceback.
|
||||
"""
|
||||
|
||||
def raises() -> None:
|
||||
raise SampleException()
|
||||
|
||||
it = maybeDeferred(raises)
|
||||
|
||||
async def doomed() -> None:
|
||||
return await it
|
||||
|
||||
failure = self.failureResultOf(Deferred.fromCoroutine(doomed()))
|
||||
|
||||
self.assertIn(", in doomed\n", failure.getTraceback())
|
||||
self.assertIn(", in raises\n", failure.getTraceback())
|
||||
|
||||
def test_asyncDeferredFailureTraceback(self) -> None:
|
||||
"""
|
||||
When a Deferred is awaited upon that later fails with a Failure that
|
||||
has a traceback, both the place that the synchronous traceback comes
|
||||
from and the awaiting line are shown in the traceback.
|
||||
"""
|
||||
|
||||
def returnsFailure() -> Failure:
|
||||
try:
|
||||
raise SampleException()
|
||||
except SampleException:
|
||||
return Failure()
|
||||
|
||||
it: Deferred[None] = Deferred()
|
||||
|
||||
async def doomed() -> None:
|
||||
return await it
|
||||
|
||||
started = Deferred.fromCoroutine(doomed())
|
||||
self.assertNoResult(started)
|
||||
it.errback(returnsFailure())
|
||||
failure = self.failureResultOf(started)
|
||||
self.assertIn(", in doomed\n", failure.getTraceback())
|
||||
self.assertIn(", in returnsFailure\n", failure.getTraceback())
|
||||
|
||||
def test_twoDeep(self) -> None:
|
||||
"""
|
||||
A coroutine scheduled with L{Deferred.fromCoroutine} that awaits a
|
||||
L{Deferred} suspends its execution until the inner L{Deferred} fires.
|
||||
"""
|
||||
reactor = Clock()
|
||||
sections = []
|
||||
|
||||
async def runone() -> str:
|
||||
sections.append(2)
|
||||
d: Deferred[int] = Deferred()
|
||||
reactor.callLater(1, d.callback, 2)
|
||||
await d
|
||||
sections.append(3)
|
||||
return "Yay!"
|
||||
|
||||
async def run() -> str:
|
||||
sections.append(1)
|
||||
result = await runone()
|
||||
sections.append(4)
|
||||
d: Deferred[int] = Deferred()
|
||||
reactor.callLater(1, d.callback, 1)
|
||||
await d
|
||||
sections.append(5)
|
||||
return result
|
||||
|
||||
d = Deferred.fromCoroutine(run())
|
||||
|
||||
reactor.advance(0.9)
|
||||
self.assertEqual(sections, [1, 2])
|
||||
|
||||
reactor.advance(0.1)
|
||||
self.assertEqual(sections, [1, 2, 3, 4])
|
||||
|
||||
reactor.advance(0.9)
|
||||
self.assertEqual(sections, [1, 2, 3, 4])
|
||||
|
||||
reactor.advance(0.1)
|
||||
self.assertEqual(sections, [1, 2, 3, 4, 5])
|
||||
|
||||
res = self.successResultOf(d)
|
||||
self.assertEqual(res, "Yay!")
|
||||
|
||||
def test_reraise(self) -> None:
|
||||
"""
|
||||
Awaiting an already failed Deferred will raise the exception.
|
||||
"""
|
||||
|
||||
async def test() -> int:
|
||||
try:
|
||||
await fail(ValueError("Boom"))
|
||||
except ValueError as e:
|
||||
self.assertEqual(e.args, ("Boom",))
|
||||
return 1
|
||||
return 0
|
||||
|
||||
res = self.successResultOf(Deferred.fromCoroutine(test()))
|
||||
self.assertEqual(res, 1)
|
||||
|
||||
def test_chained(self) -> None:
|
||||
"""
|
||||
Awaiting a paused & chained Deferred will give the result when it has
|
||||
one.
|
||||
"""
|
||||
reactor = Clock()
|
||||
|
||||
async def test() -> None:
|
||||
d: Deferred[None] = Deferred()
|
||||
d2: Deferred[None] = Deferred()
|
||||
d.addCallback(lambda ignored: d2)
|
||||
|
||||
d.callback(None)
|
||||
reactor.callLater(0, d2.callback, "bye")
|
||||
return await d
|
||||
|
||||
d = Deferred.fromCoroutine(test())
|
||||
reactor.advance(0.1)
|
||||
|
||||
res = self.successResultOf(d)
|
||||
self.assertEqual(res, "bye")
|
||||
@@ -0,0 +1,182 @@
|
||||
# -*- test-case-name: twisted.internet.test.test_defer_yieldfrom -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for C{yield from} support in Deferreds.
|
||||
"""
|
||||
|
||||
import types
|
||||
|
||||
from twisted.internet.defer import Deferred, ensureDeferred, fail, succeed
|
||||
from twisted.internet.task import Clock
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class YieldFromTests(TestCase):
|
||||
"""
|
||||
Tests for using Deferreds in conjunction with PEP-380.
|
||||
"""
|
||||
|
||||
def test_ensureDeferred(self) -> None:
|
||||
"""
|
||||
L{ensureDeferred} will turn a coroutine into a L{Deferred}.
|
||||
"""
|
||||
|
||||
def run():
|
||||
d = succeed("foo")
|
||||
res = yield from d
|
||||
return res
|
||||
|
||||
# It's a generator...
|
||||
r = run()
|
||||
self.assertIsInstance(r, types.GeneratorType)
|
||||
|
||||
# Now it's a Deferred.
|
||||
d = ensureDeferred(r)
|
||||
self.assertIsInstance(d, Deferred)
|
||||
|
||||
# The Deferred has the result we want.
|
||||
res = self.successResultOf(d)
|
||||
self.assertEqual(res, "foo")
|
||||
|
||||
def test_DeferredfromCoroutine(self) -> None:
|
||||
"""
|
||||
L{Deferred.fromCoroutine} will turn a coroutine into a L{Deferred}.
|
||||
"""
|
||||
|
||||
def run():
|
||||
d = succeed("bar")
|
||||
yield from d
|
||||
res = yield from run2()
|
||||
return res
|
||||
|
||||
def run2():
|
||||
d = succeed("foo")
|
||||
res = yield from d
|
||||
return res
|
||||
|
||||
# It's a generator...
|
||||
r = run()
|
||||
self.assertIsInstance(r, types.GeneratorType)
|
||||
|
||||
# Now it's a Deferred.
|
||||
d = Deferred.fromCoroutine(r)
|
||||
self.assertIsInstance(d, Deferred)
|
||||
|
||||
# The Deferred has the result we want.
|
||||
res = self.successResultOf(d)
|
||||
self.assertEqual(res, "foo")
|
||||
|
||||
def test_basic(self) -> None:
|
||||
"""
|
||||
L{Deferred.fromCoroutine} allows a function to C{yield from} a
|
||||
L{Deferred}.
|
||||
"""
|
||||
|
||||
def run():
|
||||
d = succeed("foo")
|
||||
res = yield from d
|
||||
return res
|
||||
|
||||
d = Deferred.fromCoroutine(run())
|
||||
res = self.successResultOf(d)
|
||||
self.assertEqual(res, "foo")
|
||||
|
||||
def test_exception(self) -> None:
|
||||
"""
|
||||
An exception in a generator scheduled with L{Deferred.fromCoroutine}
|
||||
will cause the returned L{Deferred} to fire with a failure.
|
||||
"""
|
||||
|
||||
def run():
|
||||
d = succeed("foo")
|
||||
yield from d
|
||||
raise ValueError("Oh no!")
|
||||
|
||||
d = Deferred.fromCoroutine(run())
|
||||
res = self.failureResultOf(d)
|
||||
self.assertEqual(type(res.value), ValueError)
|
||||
self.assertEqual(res.value.args, ("Oh no!",))
|
||||
|
||||
def test_twoDeep(self) -> None:
|
||||
"""
|
||||
An exception in a generator scheduled with L{Deferred.fromCoroutine}
|
||||
will cause the returned L{Deferred} to fire with a failure.
|
||||
"""
|
||||
reactor = Clock()
|
||||
sections = []
|
||||
|
||||
def runone():
|
||||
sections.append(2)
|
||||
d = Deferred()
|
||||
reactor.callLater(1, d.callback, None)
|
||||
yield from d
|
||||
sections.append(3)
|
||||
return "Yay!"
|
||||
|
||||
def run():
|
||||
sections.append(1)
|
||||
result = yield from runone()
|
||||
sections.append(4)
|
||||
d = Deferred()
|
||||
reactor.callLater(1, d.callback, None)
|
||||
yield from d
|
||||
sections.append(5)
|
||||
return result
|
||||
|
||||
d = Deferred.fromCoroutine(run())
|
||||
|
||||
reactor.advance(0.9)
|
||||
self.assertEqual(sections, [1, 2])
|
||||
|
||||
reactor.advance(0.1)
|
||||
self.assertEqual(sections, [1, 2, 3, 4])
|
||||
|
||||
reactor.advance(0.9)
|
||||
self.assertEqual(sections, [1, 2, 3, 4])
|
||||
|
||||
reactor.advance(0.1)
|
||||
self.assertEqual(sections, [1, 2, 3, 4, 5])
|
||||
|
||||
res = self.successResultOf(d)
|
||||
self.assertEqual(res, "Yay!")
|
||||
|
||||
def test_reraise(self) -> None:
|
||||
"""
|
||||
Yielding from an already failed Deferred will raise the exception.
|
||||
"""
|
||||
|
||||
def test():
|
||||
try:
|
||||
yield from fail(ValueError("Boom"))
|
||||
except ValueError as e:
|
||||
self.assertEqual(e.args, ("Boom",))
|
||||
return 1
|
||||
return 0
|
||||
|
||||
res = self.successResultOf(Deferred.fromCoroutine(test()))
|
||||
self.assertEqual(res, 1)
|
||||
|
||||
def test_chained(self) -> None:
|
||||
"""
|
||||
Yielding from a paused & chained Deferred will give the result when it
|
||||
has one.
|
||||
"""
|
||||
reactor = Clock()
|
||||
|
||||
def test():
|
||||
d = Deferred()
|
||||
d2 = Deferred()
|
||||
d.addCallback(lambda ignored: d2)
|
||||
|
||||
d.callback(None)
|
||||
reactor.callLater(0, d2.callback, "bye")
|
||||
res = yield from d
|
||||
return res
|
||||
|
||||
d = Deferred.fromCoroutine(test())
|
||||
reactor.advance(0.1)
|
||||
|
||||
res = self.successResultOf(d)
|
||||
self.assertEqual(res, "bye")
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,230 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.internet.epollreactor}.
|
||||
"""
|
||||
|
||||
from unittest import skipIf
|
||||
|
||||
from twisted.internet.error import ConnectionDone
|
||||
from twisted.internet.posixbase import _ContinuousPolling
|
||||
from twisted.internet.task import Clock
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
try:
|
||||
from twisted.internet import epollreactor
|
||||
except ImportError:
|
||||
epollreactor = None # type: ignore[assignment]
|
||||
|
||||
|
||||
class Descriptor:
|
||||
"""
|
||||
Records reads and writes, as if it were a C{FileDescriptor}.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.events = []
|
||||
|
||||
def fileno(self):
|
||||
return 1
|
||||
|
||||
def doRead(self):
|
||||
self.events.append("read")
|
||||
|
||||
def doWrite(self):
|
||||
self.events.append("write")
|
||||
|
||||
def connectionLost(self, reason):
|
||||
reason.trap(ConnectionDone)
|
||||
self.events.append("lost")
|
||||
|
||||
|
||||
@skipIf(not epollreactor, "epoll not supported in this environment.")
|
||||
class ContinuousPollingTests(TestCase):
|
||||
"""
|
||||
L{_ContinuousPolling} can be used to read and write from C{FileDescriptor}
|
||||
objects.
|
||||
"""
|
||||
|
||||
def test_addReader(self):
|
||||
"""
|
||||
Adding a reader when there was previously no reader starts up a
|
||||
C{LoopingCall}.
|
||||
"""
|
||||
poller = _ContinuousPolling(Clock())
|
||||
self.assertIsNone(poller._loop)
|
||||
reader = object()
|
||||
self.assertFalse(poller.isReading(reader))
|
||||
poller.addReader(reader)
|
||||
self.assertIsNotNone(poller._loop)
|
||||
self.assertTrue(poller._loop.running)
|
||||
self.assertIs(poller._loop.clock, poller._reactor)
|
||||
self.assertTrue(poller.isReading(reader))
|
||||
|
||||
def test_addWriter(self):
|
||||
"""
|
||||
Adding a writer when there was previously no writer starts up a
|
||||
C{LoopingCall}.
|
||||
"""
|
||||
poller = _ContinuousPolling(Clock())
|
||||
self.assertIsNone(poller._loop)
|
||||
writer = object()
|
||||
self.assertFalse(poller.isWriting(writer))
|
||||
poller.addWriter(writer)
|
||||
self.assertIsNotNone(poller._loop)
|
||||
self.assertTrue(poller._loop.running)
|
||||
self.assertIs(poller._loop.clock, poller._reactor)
|
||||
self.assertTrue(poller.isWriting(writer))
|
||||
|
||||
def test_removeReader(self):
|
||||
"""
|
||||
Removing a reader stops the C{LoopingCall}.
|
||||
"""
|
||||
poller = _ContinuousPolling(Clock())
|
||||
reader = object()
|
||||
poller.addReader(reader)
|
||||
poller.removeReader(reader)
|
||||
self.assertIsNone(poller._loop)
|
||||
self.assertEqual(poller._reactor.getDelayedCalls(), [])
|
||||
self.assertFalse(poller.isReading(reader))
|
||||
|
||||
def test_removeWriter(self):
|
||||
"""
|
||||
Removing a writer stops the C{LoopingCall}.
|
||||
"""
|
||||
poller = _ContinuousPolling(Clock())
|
||||
writer = object()
|
||||
poller.addWriter(writer)
|
||||
poller.removeWriter(writer)
|
||||
self.assertIsNone(poller._loop)
|
||||
self.assertEqual(poller._reactor.getDelayedCalls(), [])
|
||||
self.assertFalse(poller.isWriting(writer))
|
||||
|
||||
def test_removeUnknown(self):
|
||||
"""
|
||||
Removing unknown readers and writers silently does nothing.
|
||||
"""
|
||||
poller = _ContinuousPolling(Clock())
|
||||
poller.removeWriter(object())
|
||||
poller.removeReader(object())
|
||||
|
||||
def test_multipleReadersAndWriters(self):
|
||||
"""
|
||||
Adding multiple readers and writers results in a single
|
||||
C{LoopingCall}.
|
||||
"""
|
||||
poller = _ContinuousPolling(Clock())
|
||||
writer = object()
|
||||
poller.addWriter(writer)
|
||||
self.assertIsNotNone(poller._loop)
|
||||
poller.addWriter(object())
|
||||
self.assertIsNotNone(poller._loop)
|
||||
poller.addReader(object())
|
||||
self.assertIsNotNone(poller._loop)
|
||||
poller.addReader(object())
|
||||
poller.removeWriter(writer)
|
||||
self.assertIsNotNone(poller._loop)
|
||||
self.assertTrue(poller._loop.running)
|
||||
self.assertEqual(len(poller._reactor.getDelayedCalls()), 1)
|
||||
|
||||
def test_readerPolling(self):
|
||||
"""
|
||||
Adding a reader causes its C{doRead} to be called every 1
|
||||
milliseconds.
|
||||
"""
|
||||
reactor = Clock()
|
||||
poller = _ContinuousPolling(reactor)
|
||||
desc = Descriptor()
|
||||
poller.addReader(desc)
|
||||
self.assertEqual(desc.events, [])
|
||||
reactor.advance(0.00001)
|
||||
self.assertEqual(desc.events, ["read"])
|
||||
reactor.advance(0.00001)
|
||||
self.assertEqual(desc.events, ["read", "read"])
|
||||
reactor.advance(0.00001)
|
||||
self.assertEqual(desc.events, ["read", "read", "read"])
|
||||
|
||||
def test_writerPolling(self):
|
||||
"""
|
||||
Adding a writer causes its C{doWrite} to be called every 1
|
||||
milliseconds.
|
||||
"""
|
||||
reactor = Clock()
|
||||
poller = _ContinuousPolling(reactor)
|
||||
desc = Descriptor()
|
||||
poller.addWriter(desc)
|
||||
self.assertEqual(desc.events, [])
|
||||
reactor.advance(0.001)
|
||||
self.assertEqual(desc.events, ["write"])
|
||||
reactor.advance(0.001)
|
||||
self.assertEqual(desc.events, ["write", "write"])
|
||||
reactor.advance(0.001)
|
||||
self.assertEqual(desc.events, ["write", "write", "write"])
|
||||
|
||||
def test_connectionLostOnRead(self):
|
||||
"""
|
||||
If a C{doRead} returns a value indicating disconnection,
|
||||
C{connectionLost} is called on it.
|
||||
"""
|
||||
reactor = Clock()
|
||||
poller = _ContinuousPolling(reactor)
|
||||
desc = Descriptor()
|
||||
desc.doRead = lambda: ConnectionDone()
|
||||
poller.addReader(desc)
|
||||
self.assertEqual(desc.events, [])
|
||||
reactor.advance(0.001)
|
||||
self.assertEqual(desc.events, ["lost"])
|
||||
|
||||
def test_connectionLostOnWrite(self):
|
||||
"""
|
||||
If a C{doWrite} returns a value indicating disconnection,
|
||||
C{connectionLost} is called on it.
|
||||
"""
|
||||
reactor = Clock()
|
||||
poller = _ContinuousPolling(reactor)
|
||||
desc = Descriptor()
|
||||
desc.doWrite = lambda: ConnectionDone()
|
||||
poller.addWriter(desc)
|
||||
self.assertEqual(desc.events, [])
|
||||
reactor.advance(0.001)
|
||||
self.assertEqual(desc.events, ["lost"])
|
||||
|
||||
def test_removeAll(self):
|
||||
"""
|
||||
L{_ContinuousPolling.removeAll} removes all descriptors and returns
|
||||
the readers and writers.
|
||||
"""
|
||||
poller = _ContinuousPolling(Clock())
|
||||
reader = object()
|
||||
writer = object()
|
||||
both = object()
|
||||
poller.addReader(reader)
|
||||
poller.addReader(both)
|
||||
poller.addWriter(writer)
|
||||
poller.addWriter(both)
|
||||
removed = poller.removeAll()
|
||||
self.assertEqual(poller.getReaders(), [])
|
||||
self.assertEqual(poller.getWriters(), [])
|
||||
self.assertEqual(len(removed), 3)
|
||||
self.assertEqual(set(removed), {reader, writer, both})
|
||||
|
||||
def test_getReaders(self):
|
||||
"""
|
||||
L{_ContinuousPolling.getReaders} returns a list of the read
|
||||
descriptors.
|
||||
"""
|
||||
poller = _ContinuousPolling(Clock())
|
||||
reader = object()
|
||||
poller.addReader(reader)
|
||||
self.assertIn(reader, poller.getReaders())
|
||||
|
||||
def test_getWriters(self):
|
||||
"""
|
||||
L{_ContinuousPolling.getWriters} returns a list of the write
|
||||
descriptors.
|
||||
"""
|
||||
poller = _ContinuousPolling(Clock())
|
||||
writer = object()
|
||||
poller.addWriter(writer)
|
||||
self.assertIn(writer, poller.getWriters())
|
||||
@@ -0,0 +1,40 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.internet.error}
|
||||
"""
|
||||
|
||||
|
||||
from twisted.internet import error
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
|
||||
|
||||
class ConnectionAbortedTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for the L{twisted.internet.error.ConnectionAborted} exception.
|
||||
"""
|
||||
|
||||
def test_str(self) -> None:
|
||||
"""
|
||||
The default message of L{ConnectionAborted} is a sentence which points
|
||||
to L{ITCPTransport.abortConnection()}
|
||||
"""
|
||||
self.assertEqual(
|
||||
("Connection was aborted locally" " using ITCPTransport.abortConnection."),
|
||||
str(error.ConnectionAborted()),
|
||||
)
|
||||
|
||||
def test_strArgs(self) -> None:
|
||||
"""
|
||||
Any arguments passed to L{ConnectionAborted} are included in its
|
||||
message.
|
||||
"""
|
||||
self.assertEqual(
|
||||
(
|
||||
"Connection was aborted locally using"
|
||||
" ITCPTransport.abortConnection:"
|
||||
" foo bar."
|
||||
),
|
||||
str(error.ConnectionAborted("foo", "bar")),
|
||||
)
|
||||
@@ -0,0 +1,422 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for implementations of L{IReactorFDSet}.
|
||||
"""
|
||||
|
||||
import os
|
||||
import socket
|
||||
import traceback
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest import skipIf
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.internet.abstract import FileDescriptor
|
||||
from twisted.internet.interfaces import IReactorFDSet, IReadDescriptor
|
||||
from twisted.internet.tcp import EINPROGRESS, EWOULDBLOCK
|
||||
from twisted.internet.test.reactormixins import ReactorBuilder
|
||||
from twisted.python.runtime import platform
|
||||
from twisted.trial.unittest import SkipTest, SynchronousTestCase
|
||||
|
||||
if TYPE_CHECKING:
|
||||
PretendTestCase = SynchronousTestCase
|
||||
else:
|
||||
PretendTestCase = object
|
||||
|
||||
|
||||
def socketpair():
|
||||
serverSocket = socket.socket()
|
||||
serverSocket.bind(("127.0.0.1", 0))
|
||||
serverSocket.listen(1)
|
||||
try:
|
||||
client = socket.socket()
|
||||
try:
|
||||
client.setblocking(False)
|
||||
try:
|
||||
client.connect(("127.0.0.1", serverSocket.getsockname()[1]))
|
||||
except OSError as e:
|
||||
if e.args[0] not in (EINPROGRESS, EWOULDBLOCK):
|
||||
raise
|
||||
server, addr = serverSocket.accept()
|
||||
except BaseException:
|
||||
client.close()
|
||||
raise
|
||||
finally:
|
||||
serverSocket.close()
|
||||
|
||||
return client, server
|
||||
|
||||
|
||||
class ReactorFDSetTestsBuilder(ReactorBuilder, PretendTestCase):
|
||||
"""
|
||||
Builder defining tests relating to L{IReactorFDSet}.
|
||||
"""
|
||||
|
||||
requiredInterfaces = [IReactorFDSet]
|
||||
|
||||
def _connectedPair(self):
|
||||
"""
|
||||
Return the two sockets which make up a new TCP connection.
|
||||
"""
|
||||
client, server = socketpair()
|
||||
self.addCleanup(client.close)
|
||||
self.addCleanup(server.close)
|
||||
return client, server
|
||||
|
||||
def _simpleSetup(self):
|
||||
reactor = self.buildReactor()
|
||||
|
||||
client, server = self._connectedPair()
|
||||
|
||||
fd = FileDescriptor(reactor)
|
||||
fd.fileno = client.fileno
|
||||
|
||||
return reactor, fd, server
|
||||
|
||||
def test_addReader(self):
|
||||
"""
|
||||
C{reactor.addReader()} accepts an L{IReadDescriptor} provider and calls
|
||||
its C{doRead} method when there may be data available on its C{fileno}.
|
||||
"""
|
||||
reactor, fd, server = self._simpleSetup()
|
||||
|
||||
def removeAndStop():
|
||||
reactor.removeReader(fd)
|
||||
reactor.stop()
|
||||
|
||||
fd.doRead = removeAndStop
|
||||
reactor.addReader(fd)
|
||||
server.sendall(b"x")
|
||||
|
||||
# The reactor will only stop if it calls fd.doRead.
|
||||
self.runReactor(reactor)
|
||||
# Nothing to assert, just be glad we got this far.
|
||||
|
||||
def test_removeReader(self):
|
||||
"""
|
||||
L{reactor.removeReader()} accepts an L{IReadDescriptor} provider
|
||||
previously passed to C{reactor.addReader()} and causes it to no longer
|
||||
be monitored for input events.
|
||||
"""
|
||||
reactor, fd, server = self._simpleSetup()
|
||||
|
||||
def fail():
|
||||
self.fail("doRead should not be called")
|
||||
|
||||
fd.doRead = fail
|
||||
|
||||
reactor.addReader(fd)
|
||||
reactor.removeReader(fd)
|
||||
server.sendall(b"x")
|
||||
|
||||
# Give the reactor two timed event passes to notice that there's I/O
|
||||
# (if it is incorrectly watching for I/O).
|
||||
reactor.callLater(0, reactor.callLater, 0, reactor.stop)
|
||||
|
||||
self.runReactor(reactor)
|
||||
# Getting here means the right thing happened probably.
|
||||
|
||||
def test_addWriter(self):
|
||||
"""
|
||||
C{reactor.addWriter()} accepts an L{IWriteDescriptor} provider and
|
||||
calls its C{doWrite} method when it may be possible to write to its
|
||||
C{fileno}.
|
||||
"""
|
||||
reactor, fd, server = self._simpleSetup()
|
||||
|
||||
def removeAndStop():
|
||||
reactor.removeWriter(fd)
|
||||
reactor.stop()
|
||||
|
||||
fd.doWrite = removeAndStop
|
||||
reactor.addWriter(fd)
|
||||
|
||||
self.runReactor(reactor)
|
||||
# Getting here is great.
|
||||
|
||||
def _getFDTest(self, kind):
|
||||
"""
|
||||
Helper for getReaders and getWriters tests.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
get = getattr(reactor, "get" + kind + "s")
|
||||
add = getattr(reactor, "add" + kind)
|
||||
remove = getattr(reactor, "remove" + kind)
|
||||
|
||||
client, server = self._connectedPair()
|
||||
|
||||
self.assertNotIn(client, get())
|
||||
self.assertNotIn(server, get())
|
||||
|
||||
add(client)
|
||||
self.assertIn(client, get())
|
||||
self.assertNotIn(server, get())
|
||||
|
||||
remove(client)
|
||||
self.assertNotIn(client, get())
|
||||
self.assertNotIn(server, get())
|
||||
|
||||
def test_getReaders(self):
|
||||
"""
|
||||
L{IReactorFDSet.getReaders} reflects the additions and removals made
|
||||
with L{IReactorFDSet.addReader} and L{IReactorFDSet.removeReader}.
|
||||
"""
|
||||
self._getFDTest("Reader")
|
||||
|
||||
def test_removeWriter(self):
|
||||
"""
|
||||
L{reactor.removeWriter()} accepts an L{IWriteDescriptor} provider
|
||||
previously passed to C{reactor.addWriter()} and causes it to no longer
|
||||
be monitored for outputability.
|
||||
"""
|
||||
reactor, fd, server = self._simpleSetup()
|
||||
|
||||
def fail():
|
||||
self.fail("doWrite should not be called")
|
||||
|
||||
fd.doWrite = fail
|
||||
|
||||
reactor.addWriter(fd)
|
||||
reactor.removeWriter(fd)
|
||||
|
||||
# Give the reactor two timed event passes to notice that there's I/O
|
||||
# (if it is incorrectly watching for I/O).
|
||||
reactor.callLater(0, reactor.callLater, 0, reactor.stop)
|
||||
|
||||
self.runReactor(reactor)
|
||||
# Getting here means the right thing happened probably.
|
||||
|
||||
def test_getWriters(self):
|
||||
"""
|
||||
L{IReactorFDSet.getWriters} reflects the additions and removals made
|
||||
with L{IReactorFDSet.addWriter} and L{IReactorFDSet.removeWriter}.
|
||||
"""
|
||||
self._getFDTest("Writer")
|
||||
|
||||
def test_removeAll(self):
|
||||
"""
|
||||
C{reactor.removeAll()} removes all registered L{IReadDescriptor}
|
||||
providers and all registered L{IWriteDescriptor} providers and returns
|
||||
them.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
|
||||
reactor, fd, server = self._simpleSetup()
|
||||
|
||||
fd.doRead = lambda: self.fail("doRead should not be called")
|
||||
fd.doWrite = lambda: self.fail("doWrite should not be called")
|
||||
|
||||
server.sendall(b"x")
|
||||
|
||||
reactor.addReader(fd)
|
||||
reactor.addWriter(fd)
|
||||
|
||||
removed = reactor.removeAll()
|
||||
|
||||
# Give the reactor two timed event passes to notice that there's I/O
|
||||
# (if it is incorrectly watching for I/O).
|
||||
reactor.callLater(0, reactor.callLater, 0, reactor.stop)
|
||||
|
||||
self.runReactor(reactor)
|
||||
# Getting here means the right thing happened probably.
|
||||
|
||||
self.assertEqual(removed, [fd])
|
||||
|
||||
def test_removedFromReactor(self):
|
||||
"""
|
||||
A descriptor's C{fileno} method should not be called after the
|
||||
descriptor has been removed from the reactor.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
descriptor = RemovingDescriptor(reactor)
|
||||
reactor.callWhenRunning(descriptor.start)
|
||||
self.runReactor(reactor)
|
||||
self.assertEqual(descriptor.calls, [])
|
||||
|
||||
def test_negativeOneFileDescriptor(self):
|
||||
"""
|
||||
If L{FileDescriptor.fileno} returns C{-1}, the descriptor is removed
|
||||
from the reactor.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
|
||||
client, server = self._connectedPair()
|
||||
|
||||
class DisappearingDescriptor(FileDescriptor):
|
||||
_fileno = server.fileno()
|
||||
|
||||
_received = b""
|
||||
|
||||
def fileno(self):
|
||||
return self._fileno
|
||||
|
||||
def doRead(self):
|
||||
self._fileno = -1
|
||||
self._received += server.recv(1)
|
||||
client.send(b"y")
|
||||
|
||||
def connectionLost(self, reason):
|
||||
reactor.stop()
|
||||
|
||||
descriptor = DisappearingDescriptor(reactor)
|
||||
reactor.addReader(descriptor)
|
||||
client.send(b"x")
|
||||
self.runReactor(reactor)
|
||||
self.assertEqual(descriptor._received, b"x")
|
||||
|
||||
@skipIf(platform.isWindows(), "Cannot duplicate socket filenos on Windows")
|
||||
def test_lostFileDescriptor(self):
|
||||
"""
|
||||
The file descriptor underlying a FileDescriptor may be closed and
|
||||
replaced by another at some point. Bytes which arrive on the new
|
||||
descriptor must not be delivered to the FileDescriptor which was
|
||||
originally registered with the original descriptor of the same number.
|
||||
|
||||
Practically speaking, this is difficult or impossible to detect. The
|
||||
implementation relies on C{fileno} raising an exception if the original
|
||||
descriptor has gone away. If C{fileno} continues to return the original
|
||||
file descriptor value, the reactor may deliver events from that
|
||||
descriptor. This is a best effort attempt to ease certain debugging
|
||||
situations. Applications should not rely on it intentionally.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
|
||||
name = reactor.__class__.__name__
|
||||
if name in (
|
||||
"EPollReactor",
|
||||
"KQueueReactor",
|
||||
"CFReactor",
|
||||
"AsyncioSelectorReactor",
|
||||
):
|
||||
# Closing a file descriptor immediately removes it from the epoll
|
||||
# set without generating a notification. That means epollreactor
|
||||
# will not call any methods on Victim after the close, so there's
|
||||
# no chance to notice the socket is no longer valid.
|
||||
raise SkipTest(f"{name!r} cannot detect lost file descriptors")
|
||||
|
||||
client, server = self._connectedPair()
|
||||
|
||||
class Victim(FileDescriptor):
|
||||
"""
|
||||
This L{FileDescriptor} will have its socket closed out from under it
|
||||
and another socket will take its place. It will raise a
|
||||
socket.error from C{fileno} after this happens (because socket
|
||||
objects remember whether they have been closed), so as long as the
|
||||
reactor calls the C{fileno} method the problem will be detected.
|
||||
"""
|
||||
|
||||
def fileno(self):
|
||||
return server.fileno()
|
||||
|
||||
def doRead(self):
|
||||
raise Exception("Victim.doRead should never be called")
|
||||
|
||||
def connectionLost(self, reason):
|
||||
"""
|
||||
When the problem is detected, the reactor should disconnect this
|
||||
file descriptor. When that happens, stop the reactor so the
|
||||
test ends.
|
||||
"""
|
||||
reactor.stop()
|
||||
|
||||
reactor.addReader(Victim())
|
||||
|
||||
# Arrange for the socket to be replaced at some unspecified time.
|
||||
# Significantly, this will not be while any I/O processing code is on
|
||||
# the stack. It is something that happens independently and cannot be
|
||||
# relied upon to happen at a convenient time, such as within a call to
|
||||
# doRead.
|
||||
def messItUp():
|
||||
newC, newS = self._connectedPair()
|
||||
fileno = server.fileno()
|
||||
server.close()
|
||||
os.dup2(newS.fileno(), fileno)
|
||||
newC.send(b"x")
|
||||
|
||||
reactor.callLater(0, messItUp)
|
||||
|
||||
self.runReactor(reactor)
|
||||
|
||||
# If the implementation feels like logging the exception raised by
|
||||
# MessedUp.fileno, that's fine.
|
||||
self.flushLoggedErrors(socket.error)
|
||||
|
||||
def test_connectionLostOnShutdown(self):
|
||||
"""
|
||||
Any file descriptors added to the reactor have their C{connectionLost}
|
||||
called when C{reactor.stop} is called.
|
||||
"""
|
||||
reactor = self.buildReactor()
|
||||
|
||||
class DoNothingDescriptor(FileDescriptor):
|
||||
def doRead(self):
|
||||
return None
|
||||
|
||||
def doWrite(self):
|
||||
return None
|
||||
|
||||
client, server = self._connectedPair()
|
||||
|
||||
fd1 = DoNothingDescriptor(reactor)
|
||||
fd1.fileno = client.fileno
|
||||
fd2 = DoNothingDescriptor(reactor)
|
||||
fd2.fileno = server.fileno
|
||||
reactor.addReader(fd1)
|
||||
reactor.addWriter(fd2)
|
||||
|
||||
reactor.callWhenRunning(reactor.stop)
|
||||
self.runReactor(reactor)
|
||||
self.assertTrue(fd1.disconnected)
|
||||
self.assertTrue(fd2.disconnected)
|
||||
|
||||
|
||||
@implementer(IReadDescriptor)
|
||||
class RemovingDescriptor:
|
||||
"""
|
||||
A read descriptor which removes itself from the reactor as soon as it
|
||||
gets a chance to do a read and keeps track of when its own C{fileno}
|
||||
method is called.
|
||||
|
||||
@ivar insideReactor: A flag which is true as long as the reactor has
|
||||
this descriptor as a reader.
|
||||
|
||||
@ivar calls: A list of the bottom of the call stack for any call to
|
||||
C{fileno} when C{insideReactor} is false.
|
||||
"""
|
||||
|
||||
def __init__(self, reactor):
|
||||
self.reactor = reactor
|
||||
self.insideReactor = False
|
||||
self.calls = []
|
||||
self.read, self.write = socketpair()
|
||||
|
||||
def start(self):
|
||||
self.insideReactor = True
|
||||
self.reactor.addReader(self)
|
||||
self.write.send(b"a")
|
||||
|
||||
def logPrefix(self):
|
||||
return "foo"
|
||||
|
||||
def doRead(self):
|
||||
self.reactor.removeReader(self)
|
||||
self.insideReactor = False
|
||||
self.reactor.stop()
|
||||
self.read.close()
|
||||
self.write.close()
|
||||
|
||||
def fileno(self):
|
||||
if not self.insideReactor:
|
||||
self.calls.append(traceback.extract_stack(limit=5)[:-1])
|
||||
return self.read.fileno()
|
||||
|
||||
def connectionLost(self, reason):
|
||||
# Ideally we'd close the descriptors here... but actually
|
||||
# connectionLost is never called because we remove ourselves from the
|
||||
# reactor before it stops.
|
||||
pass
|
||||
|
||||
|
||||
globals().update(ReactorFDSetTestsBuilder.makeTestCaseClasses())
|
||||
@@ -0,0 +1,94 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Whitebox tests for L{twisted.internet.abstract.FileDescriptor}.
|
||||
"""
|
||||
|
||||
|
||||
from zope.interface.verify import verifyClass
|
||||
|
||||
from twisted.internet.abstract import FileDescriptor
|
||||
from twisted.internet.interfaces import IPushProducer
|
||||
from twisted.trial.unittest import SynchronousTestCase
|
||||
|
||||
|
||||
class MemoryFile(FileDescriptor):
|
||||
"""
|
||||
A L{FileDescriptor} customization which writes to a Python list in memory
|
||||
with certain limitations.
|
||||
|
||||
@ivar _written: A C{list} of C{bytes} which have been accepted as written.
|
||||
|
||||
@ivar _freeSpace: A C{int} giving the number of bytes which will be accepted
|
||||
by future writes.
|
||||
"""
|
||||
|
||||
connected = True
|
||||
|
||||
def __init__(self):
|
||||
FileDescriptor.__init__(self, reactor=object())
|
||||
self._written = []
|
||||
self._freeSpace = 0
|
||||
|
||||
def startWriting(self):
|
||||
pass
|
||||
|
||||
def stopWriting(self):
|
||||
pass
|
||||
|
||||
def writeSomeData(self, data):
|
||||
"""
|
||||
Copy at most C{self._freeSpace} bytes from C{data} into C{self._written}.
|
||||
|
||||
@return: A C{int} indicating how many bytes were copied from C{data}.
|
||||
"""
|
||||
acceptLength = min(self._freeSpace, len(data))
|
||||
if acceptLength:
|
||||
self._freeSpace -= acceptLength
|
||||
self._written.append(data[:acceptLength])
|
||||
return acceptLength
|
||||
|
||||
|
||||
class FileDescriptorTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{FileDescriptor}.
|
||||
"""
|
||||
|
||||
def test_writeWithUnicodeRaisesException(self):
|
||||
"""
|
||||
L{FileDescriptor.write} doesn't accept unicode data.
|
||||
"""
|
||||
fileDescriptor = FileDescriptor(reactor=object())
|
||||
self.assertRaises(TypeError, fileDescriptor.write, "foo")
|
||||
|
||||
def test_writeSequenceWithUnicodeRaisesException(self):
|
||||
"""
|
||||
L{FileDescriptor.writeSequence} doesn't accept unicode data.
|
||||
"""
|
||||
fileDescriptor = FileDescriptor(reactor=object())
|
||||
self.assertRaises(
|
||||
TypeError, fileDescriptor.writeSequence, [b"foo", "bar", b"baz"]
|
||||
)
|
||||
|
||||
def test_implementInterfaceIPushProducer(self):
|
||||
"""
|
||||
L{FileDescriptor} should implement L{IPushProducer}.
|
||||
"""
|
||||
self.assertTrue(verifyClass(IPushProducer, FileDescriptor))
|
||||
|
||||
|
||||
class WriteDescriptorTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{FileDescriptor}'s implementation of L{IWriteDescriptor}.
|
||||
"""
|
||||
|
||||
def test_kernelBufferFull(self):
|
||||
"""
|
||||
When L{FileDescriptor.writeSomeData} returns C{0} to indicate no more
|
||||
data can be written immediately, L{FileDescriptor.doWrite} returns
|
||||
L{None}.
|
||||
"""
|
||||
descriptor = MemoryFile()
|
||||
descriptor.write(b"hello, world")
|
||||
self.assertIsNone(descriptor.doWrite())
|
||||
@@ -0,0 +1,216 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
GObject Introspection reactor tests; i.e. `gireactor` module for gio/glib/gtk
|
||||
integration.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest import skipIf
|
||||
|
||||
try:
|
||||
from gi.repository import Gio
|
||||
except ImportError:
|
||||
giImported = False
|
||||
gtkVersion = None
|
||||
else:
|
||||
giImported = True
|
||||
# If we can import Gio, we ought to be able to import our reactor.
|
||||
from os import environ
|
||||
|
||||
from gi import get_required_version, require_version
|
||||
|
||||
from twisted.internet import gireactor
|
||||
|
||||
def requireEach(someVersion: str) -> str:
|
||||
try:
|
||||
require_version("Gtk", someVersion)
|
||||
except ValueError as ve:
|
||||
return str(ve)
|
||||
else:
|
||||
return ""
|
||||
|
||||
errorMessage = ", ".join(
|
||||
requireEach(version)
|
||||
for version in environ.get("TWISTED_TEST_GTK_VERSION", "4.0,3.0").split(",")
|
||||
)
|
||||
|
||||
actualVersion = get_required_version("Gtk")
|
||||
gtkVersion = actualVersion if actualVersion is not None else errorMessage
|
||||
|
||||
|
||||
from twisted.internet.error import ReactorAlreadyRunning
|
||||
from twisted.internet.test.reactormixins import ReactorBuilder
|
||||
from twisted.trial.unittest import SkipTest, TestCase
|
||||
|
||||
# Skip all tests if gi is unavailable:
|
||||
if not giImported:
|
||||
skip = "GObject Introspection `gi` module not importable"
|
||||
|
||||
noGtkSkip = (gtkVersion is None) or (gtkVersion not in ("3.0", "4.0"))
|
||||
noGtkMessage = f"Unknown GTK version: {repr(gtkVersion)}"
|
||||
|
||||
if not noGtkSkip:
|
||||
from gi.repository import Gtk
|
||||
|
||||
|
||||
class GApplicationRegistrationTests(ReactorBuilder, TestCase):
|
||||
"""
|
||||
GtkApplication and GApplication are supported by
|
||||
L{twisted.internet.gtk3reactor} and L{twisted.internet.gireactor}.
|
||||
|
||||
We inherit from L{ReactorBuilder} in order to use some of its
|
||||
reactor-running infrastructure, but don't need its test-creation
|
||||
functionality.
|
||||
"""
|
||||
|
||||
def runReactor( # type: ignore[override]
|
||||
self,
|
||||
app: Gio.Application,
|
||||
reactor: gireactor.GIReactor,
|
||||
) -> None:
|
||||
"""
|
||||
Register the app, run the reactor, make sure app was activated, and
|
||||
that reactor was running, and that reactor can be stopped.
|
||||
"""
|
||||
if not hasattr(app, "quit"):
|
||||
raise SkipTest("Version of PyGObject is too old.")
|
||||
|
||||
result = []
|
||||
|
||||
def stop() -> None:
|
||||
result.append("stopped")
|
||||
reactor.stop()
|
||||
|
||||
def activate(widget: object) -> None:
|
||||
result.append("activated")
|
||||
reactor.callLater(0, stop)
|
||||
|
||||
app.connect("activate", activate)
|
||||
|
||||
# We want reactor.stop() to *always* stop the event loop, even if
|
||||
# someone has called hold() on the application and never done the
|
||||
# corresponding release() -- for more details see
|
||||
# http://developer.gnome.org/gio/unstable/GApplication.html.
|
||||
app.hold()
|
||||
|
||||
reactor.registerGApplication(app)
|
||||
ReactorBuilder.runReactor(self, reactor)
|
||||
self.assertEqual(result, ["activated", "stopped"])
|
||||
|
||||
def test_gApplicationActivate(self) -> None:
|
||||
"""
|
||||
L{Gio.Application} instances can be registered with a gireactor.
|
||||
"""
|
||||
self.reactorFactory = lambda: gireactor.GIReactor(useGtk=False)
|
||||
reactor = self.buildReactor()
|
||||
app = Gio.Application(
|
||||
application_id="com.twistedmatrix.trial.gireactor",
|
||||
flags=Gio.ApplicationFlags.FLAGS_NONE,
|
||||
)
|
||||
|
||||
self.runReactor(app, reactor)
|
||||
|
||||
@skipIf(noGtkSkip, noGtkMessage)
|
||||
def test_gtkAliases(self) -> None:
|
||||
"""
|
||||
L{twisted.internet.gtk3reactor} is now just a set of compatibility
|
||||
aliases for L{twisted.internet.GIReactor}.
|
||||
"""
|
||||
from twisted.internet.gtk3reactor import (
|
||||
Gtk3Reactor,
|
||||
PortableGtk3Reactor,
|
||||
install,
|
||||
)
|
||||
|
||||
self.assertIs(Gtk3Reactor, gireactor.GIReactor)
|
||||
self.assertIs(PortableGtk3Reactor, gireactor.PortableGIReactor)
|
||||
self.assertIs(install, gireactor.install)
|
||||
warnings = self.flushWarnings()
|
||||
self.assertEqual(len(warnings), 1)
|
||||
self.assertIn(
|
||||
"twisted.internet.gtk3reactor was deprecated", warnings[0]["message"]
|
||||
)
|
||||
|
||||
@skipIf(noGtkSkip, noGtkMessage)
|
||||
def test_gtkApplicationActivate(self) -> None:
|
||||
"""
|
||||
L{Gtk.Application} instances can be registered with a gtk3reactor.
|
||||
"""
|
||||
self.reactorFactory = gireactor.GIReactor
|
||||
reactor = self.buildReactor()
|
||||
app = Gtk.Application(
|
||||
application_id="com.twistedmatrix.trial.gtk3reactor",
|
||||
flags=Gio.ApplicationFlags.FLAGS_NONE,
|
||||
)
|
||||
self.runReactor(app, reactor)
|
||||
|
||||
def test_portable(self) -> None:
|
||||
"""
|
||||
L{gireactor.PortableGIReactor} doesn't support application
|
||||
registration at this time.
|
||||
"""
|
||||
self.reactorFactory = gireactor.PortableGIReactor
|
||||
reactor = self.buildReactor()
|
||||
app = Gio.Application(
|
||||
application_id="com.twistedmatrix.trial.gireactor",
|
||||
flags=Gio.ApplicationFlags.FLAGS_NONE,
|
||||
)
|
||||
self.assertRaises(NotImplementedError, reactor.registerGApplication, app)
|
||||
|
||||
def test_noQuit(self) -> None:
|
||||
"""
|
||||
Older versions of PyGObject lack C{Application.quit}, and so won't
|
||||
allow registration.
|
||||
"""
|
||||
self.reactorFactory = lambda: gireactor.GIReactor(useGtk=False)
|
||||
reactor = self.buildReactor()
|
||||
# An app with no "quit" method:
|
||||
app = object()
|
||||
exc = self.assertRaises(RuntimeError, reactor.registerGApplication, app)
|
||||
self.assertTrue(exc.args[0].startswith("Application registration is not"))
|
||||
|
||||
def test_cantRegisterAfterRun(self) -> None:
|
||||
"""
|
||||
It is not possible to register a C{Application} after the reactor has
|
||||
already started.
|
||||
"""
|
||||
self.reactorFactory = lambda: gireactor.GIReactor(useGtk=False)
|
||||
reactor = self.buildReactor()
|
||||
app = Gio.Application(
|
||||
application_id="com.twistedmatrix.trial.gireactor",
|
||||
flags=Gio.ApplicationFlags.FLAGS_NONE,
|
||||
)
|
||||
|
||||
def tryRegister() -> None:
|
||||
exc = self.assertRaises(
|
||||
ReactorAlreadyRunning, reactor.registerGApplication, app
|
||||
)
|
||||
self.assertEqual(
|
||||
exc.args[0], "Can't register application after reactor was started."
|
||||
)
|
||||
reactor.stop()
|
||||
|
||||
reactor.callLater(0, tryRegister)
|
||||
ReactorBuilder.runReactor(self, reactor)
|
||||
|
||||
def test_cantRegisterTwice(self) -> None:
|
||||
"""
|
||||
It is not possible to register more than one C{Application}.
|
||||
"""
|
||||
self.reactorFactory = lambda: gireactor.GIReactor(useGtk=False)
|
||||
reactor = self.buildReactor()
|
||||
app = Gio.Application(
|
||||
application_id="com.twistedmatrix.trial.gireactor",
|
||||
flags=Gio.ApplicationFlags.FLAGS_NONE,
|
||||
)
|
||||
reactor.registerGApplication(app)
|
||||
app2 = Gio.Application(
|
||||
application_id="com.twistedmatrix.trial.gireactor2",
|
||||
flags=Gio.ApplicationFlags.FLAGS_NONE,
|
||||
)
|
||||
exc = self.assertRaises(RuntimeError, reactor.registerGApplication, app2)
|
||||
self.assertEqual(
|
||||
exc.args[0], "Can't register more than one application instance."
|
||||
)
|
||||
@@ -0,0 +1,100 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for twisted.internet.glibbase.
|
||||
"""
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
from twisted.internet._glibbase import ensureNotImported
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class EnsureNotImportedTests(TestCase):
|
||||
"""
|
||||
L{ensureNotImported} protects against unwanted past and future imports.
|
||||
"""
|
||||
|
||||
def test_ensureWhenNotImported(self):
|
||||
"""
|
||||
If the specified modules have never been imported, and import
|
||||
prevention is requested, L{ensureNotImported} makes sure they will not
|
||||
be imported in the future.
|
||||
"""
|
||||
modules = {}
|
||||
self.patch(sys, "modules", modules)
|
||||
ensureNotImported(["m1", "m2"], "A message.", preventImports=["m1", "m2", "m3"])
|
||||
self.assertEqual(modules, {"m1": None, "m2": None, "m3": None})
|
||||
|
||||
def test_ensureWhenNotImportedDontPrevent(self):
|
||||
"""
|
||||
If the specified modules have never been imported, and import
|
||||
prevention is not requested, L{ensureNotImported} has no effect.
|
||||
"""
|
||||
modules = {}
|
||||
self.patch(sys, "modules", modules)
|
||||
ensureNotImported(["m1", "m2"], "A message.")
|
||||
self.assertEqual(modules, {})
|
||||
|
||||
def test_ensureWhenFailedToImport(self):
|
||||
"""
|
||||
If the specified modules have been set to L{None} in C{sys.modules},
|
||||
L{ensureNotImported} does not complain.
|
||||
"""
|
||||
modules = {"m2": None}
|
||||
self.patch(sys, "modules", modules)
|
||||
ensureNotImported(["m1", "m2"], "A message.", preventImports=["m1", "m2"])
|
||||
self.assertEqual(modules, {"m1": None, "m2": None})
|
||||
|
||||
def test_ensureFailsWhenImported(self):
|
||||
"""
|
||||
If one of the specified modules has been previously imported,
|
||||
L{ensureNotImported} raises an exception.
|
||||
"""
|
||||
module = object()
|
||||
modules = {"m2": module}
|
||||
self.patch(sys, "modules", modules)
|
||||
e = self.assertRaises(
|
||||
ImportError,
|
||||
ensureNotImported,
|
||||
["m1", "m2"],
|
||||
"A message.",
|
||||
preventImports=["m1", "m2"],
|
||||
)
|
||||
self.assertEqual(modules, {"m2": module})
|
||||
self.assertEqual(e.args, ("A message.",))
|
||||
|
||||
|
||||
try:
|
||||
from twisted.internet import gireactor as _gireactor
|
||||
except ImportError:
|
||||
gireactor = None
|
||||
else:
|
||||
gireactor = _gireactor
|
||||
|
||||
missingGlibReactor = None
|
||||
if gireactor is None:
|
||||
missingGlibReactor = "gi reactor not available"
|
||||
|
||||
|
||||
class GlibReactorBaseTests(TestCase):
|
||||
"""
|
||||
Tests for the private C{twisted.internet._glibbase.GlibReactorBase}
|
||||
done via the public C{twisted.internet.gireactor.PortableGIReactor}
|
||||
"""
|
||||
|
||||
skip = missingGlibReactor
|
||||
|
||||
def test_simulate(self):
|
||||
"""
|
||||
C{simulate} can be called without raising any errors when there are
|
||||
no delayed calls for the reactor and hence there is no defined sleep
|
||||
period.
|
||||
"""
|
||||
sut = gireactor.PortableGIReactor(useGtk=False)
|
||||
# Double check that reactor has no sleep period.
|
||||
self.assertIs(None, sut.timeout())
|
||||
|
||||
sut.simulate()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,569 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for the inotify wrapper in L{twisted.internet.inotify}.
|
||||
"""
|
||||
import sys
|
||||
|
||||
from twisted.internet import defer, reactor
|
||||
from twisted.python import filepath, runtime
|
||||
from twisted.python.reflect import requireModule
|
||||
from twisted.trial import unittest
|
||||
|
||||
if requireModule("twisted.python._inotify") is not None:
|
||||
from twisted.internet import inotify
|
||||
else:
|
||||
inotify = None # type: ignore[assignment]
|
||||
|
||||
|
||||
class INotifyTests(unittest.TestCase):
|
||||
"""
|
||||
Define all the tests for the basic functionality exposed by
|
||||
L{inotify.INotify}.
|
||||
"""
|
||||
|
||||
if not runtime.platform.supportsINotify():
|
||||
skip = "This platform doesn't support INotify."
|
||||
|
||||
def setUp(self):
|
||||
self.dirname = filepath.FilePath(self.mktemp())
|
||||
self.dirname.createDirectory()
|
||||
self.inotify = inotify.INotify()
|
||||
self.inotify.startReading()
|
||||
self.addCleanup(self.inotify.loseConnection)
|
||||
|
||||
def test_initializationErrors(self):
|
||||
"""
|
||||
L{inotify.INotify} emits a C{RuntimeError} when initialized
|
||||
in an environment that doesn't support inotify as we expect it.
|
||||
|
||||
We just try to raise an exception for every possible case in
|
||||
the for loop in L{inotify.INotify._inotify__init__}.
|
||||
"""
|
||||
|
||||
class FakeINotify:
|
||||
def init(self):
|
||||
raise inotify.INotifyError()
|
||||
|
||||
self.patch(inotify.INotify, "_inotify", FakeINotify())
|
||||
self.assertRaises(inotify.INotifyError, inotify.INotify)
|
||||
|
||||
def _notificationTest(self, mask, operation, expectedPath=None):
|
||||
"""
|
||||
Test notification from some filesystem operation.
|
||||
|
||||
@param mask: The event mask to use when setting up the watch.
|
||||
|
||||
@param operation: A function which will be called with the
|
||||
name of a file in the watched directory and which should
|
||||
trigger the event.
|
||||
|
||||
@param expectedPath: Optionally, the name of the path which is
|
||||
expected to come back in the notification event; this will
|
||||
also be passed to C{operation} (primarily useful when the
|
||||
operation is being done to the directory itself, not a
|
||||
file in it).
|
||||
|
||||
@return: A L{Deferred} which fires successfully when the
|
||||
expected event has been received or fails otherwise.
|
||||
"""
|
||||
if expectedPath is None:
|
||||
expectedPath = self.dirname.child("foo.bar")
|
||||
notified = defer.Deferred()
|
||||
|
||||
def cbNotified(result):
|
||||
(watch, filename, events) = result
|
||||
self.assertEqual(filename.asBytesMode(), expectedPath.asBytesMode())
|
||||
self.assertTrue(events & mask)
|
||||
|
||||
notified.addCallback(cbNotified)
|
||||
|
||||
self.inotify.watch(
|
||||
self.dirname, mask=mask, callbacks=[lambda *args: notified.callback(args)]
|
||||
)
|
||||
operation(expectedPath)
|
||||
return notified
|
||||
|
||||
def test_access(self):
|
||||
"""
|
||||
Reading from a file in a monitored directory sends an
|
||||
C{inotify.IN_ACCESS} event to the callback.
|
||||
"""
|
||||
|
||||
def operation(path):
|
||||
path.setContent(b"foo")
|
||||
path.getContent()
|
||||
|
||||
return self._notificationTest(inotify.IN_ACCESS, operation)
|
||||
|
||||
def test_modify(self):
|
||||
"""
|
||||
Writing to a file in a monitored directory sends an
|
||||
C{inotify.IN_MODIFY} event to the callback.
|
||||
"""
|
||||
|
||||
def operation(path):
|
||||
with path.open("w") as fObj:
|
||||
fObj.write(b"foo")
|
||||
|
||||
return self._notificationTest(inotify.IN_MODIFY, operation)
|
||||
|
||||
def test_attrib(self):
|
||||
"""
|
||||
Changing the metadata of a file in a monitored directory
|
||||
sends an C{inotify.IN_ATTRIB} event to the callback.
|
||||
"""
|
||||
|
||||
def operation(path):
|
||||
path.touch()
|
||||
path.touch()
|
||||
|
||||
return self._notificationTest(inotify.IN_ATTRIB, operation)
|
||||
|
||||
def test_closeWrite(self):
|
||||
"""
|
||||
Closing a file which was open for writing in a monitored
|
||||
directory sends an C{inotify.IN_CLOSE_WRITE} event to the
|
||||
callback.
|
||||
"""
|
||||
|
||||
def operation(path):
|
||||
path.open("w").close()
|
||||
|
||||
return self._notificationTest(inotify.IN_CLOSE_WRITE, operation)
|
||||
|
||||
def test_closeNoWrite(self):
|
||||
"""
|
||||
Closing a file which was open for reading but not writing in a
|
||||
monitored directory sends an C{inotify.IN_CLOSE_NOWRITE} event
|
||||
to the callback.
|
||||
"""
|
||||
|
||||
def operation(path):
|
||||
path.touch()
|
||||
path.open("r").close()
|
||||
|
||||
return self._notificationTest(inotify.IN_CLOSE_NOWRITE, operation)
|
||||
|
||||
def test_open(self):
|
||||
"""
|
||||
Opening a file in a monitored directory sends an
|
||||
C{inotify.IN_OPEN} event to the callback.
|
||||
"""
|
||||
|
||||
def operation(path):
|
||||
path.open("w").close()
|
||||
|
||||
return self._notificationTest(inotify.IN_OPEN, operation)
|
||||
|
||||
def test_movedFrom(self):
|
||||
"""
|
||||
Moving a file out of a monitored directory sends an
|
||||
C{inotify.IN_MOVED_FROM} event to the callback.
|
||||
"""
|
||||
|
||||
def operation(path):
|
||||
path.open("w").close()
|
||||
path.moveTo(filepath.FilePath(self.mktemp()))
|
||||
|
||||
return self._notificationTest(inotify.IN_MOVED_FROM, operation)
|
||||
|
||||
def test_movedTo(self):
|
||||
"""
|
||||
Moving a file into a monitored directory sends an
|
||||
C{inotify.IN_MOVED_TO} event to the callback.
|
||||
"""
|
||||
|
||||
def operation(path):
|
||||
p = filepath.FilePath(self.mktemp())
|
||||
p.touch()
|
||||
p.moveTo(path)
|
||||
|
||||
return self._notificationTest(inotify.IN_MOVED_TO, operation)
|
||||
|
||||
def test_create(self):
|
||||
"""
|
||||
Creating a file in a monitored directory sends an
|
||||
C{inotify.IN_CREATE} event to the callback.
|
||||
"""
|
||||
|
||||
def operation(path):
|
||||
path.open("w").close()
|
||||
|
||||
return self._notificationTest(inotify.IN_CREATE, operation)
|
||||
|
||||
def test_delete(self):
|
||||
"""
|
||||
Deleting a file in a monitored directory sends an
|
||||
C{inotify.IN_DELETE} event to the callback.
|
||||
"""
|
||||
|
||||
def operation(path):
|
||||
path.touch()
|
||||
path.remove()
|
||||
|
||||
return self._notificationTest(inotify.IN_DELETE, operation)
|
||||
|
||||
def test_deleteSelf(self):
|
||||
"""
|
||||
Deleting the monitored directory itself sends an
|
||||
C{inotify.IN_DELETE_SELF} event to the callback.
|
||||
"""
|
||||
|
||||
def operation(path):
|
||||
path.remove()
|
||||
|
||||
return self._notificationTest(
|
||||
inotify.IN_DELETE_SELF, operation, expectedPath=self.dirname
|
||||
)
|
||||
|
||||
def test_deleteSelfForced(self):
|
||||
"""
|
||||
Deleting the monitored directory itself sends an
|
||||
C{inotify.IN_DELETE_SELF} event to the callback
|
||||
even if the mask isn't specified by the call to watch().
|
||||
"""
|
||||
|
||||
def cbNotified(result):
|
||||
(watch, filename, events) = result
|
||||
self.assertEqual(filename.asBytesMode(), self.dirname.asBytesMode())
|
||||
self.assertTrue(events & inotify.IN_DELETE_SELF)
|
||||
|
||||
self.inotify.watch(
|
||||
self.dirname, mask=0x0, callbacks=[lambda *args: d.callback(args)]
|
||||
)
|
||||
|
||||
d = defer.Deferred()
|
||||
d.addCallback(cbNotified)
|
||||
|
||||
self.dirname.remove()
|
||||
return d
|
||||
|
||||
def test_moveSelf(self):
|
||||
"""
|
||||
Renaming the monitored directory itself sends an
|
||||
C{inotify.IN_MOVE_SELF} event to the callback.
|
||||
"""
|
||||
|
||||
def operation(path):
|
||||
path.moveTo(filepath.FilePath(self.mktemp()))
|
||||
|
||||
return self._notificationTest(
|
||||
inotify.IN_MOVE_SELF, operation, expectedPath=self.dirname
|
||||
)
|
||||
|
||||
def test_simpleSubdirectoryAutoAdd(self):
|
||||
"""
|
||||
L{inotify.INotify} when initialized with autoAdd==True adds
|
||||
also adds the created subdirectories to the watchlist.
|
||||
"""
|
||||
|
||||
def _callback(wp, filename, mask):
|
||||
# We are notified before we actually process new
|
||||
# directories, so we need to defer this check.
|
||||
def _():
|
||||
try:
|
||||
self.assertTrue(self.inotify._isWatched(subdir))
|
||||
d.callback(None)
|
||||
except Exception:
|
||||
d.errback()
|
||||
|
||||
reactor.callLater(0, _)
|
||||
|
||||
checkMask = inotify.IN_ISDIR | inotify.IN_CREATE
|
||||
self.inotify.watch(
|
||||
self.dirname, mask=checkMask, autoAdd=True, callbacks=[_callback]
|
||||
)
|
||||
subdir = self.dirname.child("test")
|
||||
d = defer.Deferred()
|
||||
subdir.createDirectory()
|
||||
return d
|
||||
|
||||
def test_simpleDeleteDirectory(self):
|
||||
"""
|
||||
L{inotify.INotify} removes a directory from the watchlist when
|
||||
it's removed from the filesystem.
|
||||
"""
|
||||
calls = []
|
||||
|
||||
def _callback(wp, filename, mask):
|
||||
# We are notified before we actually process new
|
||||
# directories, so we need to defer this check.
|
||||
def _():
|
||||
try:
|
||||
self.assertTrue(self.inotify._isWatched(subdir))
|
||||
subdir.remove()
|
||||
except Exception:
|
||||
d.errback()
|
||||
|
||||
def _eb():
|
||||
# second call, we have just removed the subdir
|
||||
try:
|
||||
self.assertFalse(self.inotify._isWatched(subdir))
|
||||
d.callback(None)
|
||||
except Exception:
|
||||
d.errback()
|
||||
|
||||
if not calls:
|
||||
# first call, it's the create subdir
|
||||
calls.append(filename)
|
||||
reactor.callLater(0, _)
|
||||
|
||||
else:
|
||||
reactor.callLater(0, _eb)
|
||||
|
||||
checkMask = inotify.IN_ISDIR | inotify.IN_CREATE
|
||||
self.inotify.watch(
|
||||
self.dirname, mask=checkMask, autoAdd=True, callbacks=[_callback]
|
||||
)
|
||||
subdir = self.dirname.child("test")
|
||||
d = defer.Deferred()
|
||||
subdir.createDirectory()
|
||||
return d
|
||||
|
||||
def test_deleteSelfLoseConnection(self):
|
||||
"""
|
||||
L{inotify.INotify} closes the file descriptor after removing a
|
||||
directory from the filesystem (and therefore from the watchlist).
|
||||
"""
|
||||
|
||||
def cbNotified(result):
|
||||
def _():
|
||||
try:
|
||||
self.assertFalse(self.inotify._isWatched(self.dirname))
|
||||
self.assertFalse(self.inotify.connected)
|
||||
d.callback(None)
|
||||
except Exception:
|
||||
d.errback()
|
||||
|
||||
(ignored, filename, events) = result
|
||||
self.assertEqual(filename.asBytesMode(), self.dirname.asBytesMode())
|
||||
self.assertTrue(events & inotify.IN_DELETE_SELF)
|
||||
reactor.callLater(0, _)
|
||||
|
||||
self.assertTrue(
|
||||
self.inotify.watch(
|
||||
self.dirname,
|
||||
mask=inotify.IN_DELETE_SELF,
|
||||
callbacks=[lambda *args: notified.callback(args)],
|
||||
)
|
||||
)
|
||||
|
||||
notified = defer.Deferred()
|
||||
notified.addCallback(cbNotified)
|
||||
|
||||
self.dirname.remove()
|
||||
d = defer.Deferred()
|
||||
return d
|
||||
|
||||
def test_ignoreDirectory(self):
|
||||
"""
|
||||
L{inotify.INotify.ignore} removes a directory from the watchlist
|
||||
"""
|
||||
self.inotify.watch(self.dirname, autoAdd=True)
|
||||
self.assertTrue(self.inotify._isWatched(self.dirname))
|
||||
self.inotify.ignore(self.dirname)
|
||||
self.assertFalse(self.inotify._isWatched(self.dirname))
|
||||
|
||||
def test_humanReadableMask(self):
|
||||
"""
|
||||
L{inotify.humaReadableMask} translates all the possible event
|
||||
masks to a human readable string.
|
||||
"""
|
||||
for mask, value in inotify._FLAG_TO_HUMAN:
|
||||
self.assertEqual(inotify.humanReadableMask(mask)[0], value)
|
||||
|
||||
checkMask = inotify.IN_CLOSE_WRITE | inotify.IN_ACCESS | inotify.IN_OPEN
|
||||
self.assertEqual(
|
||||
set(inotify.humanReadableMask(checkMask)),
|
||||
{"close_write", "access", "open"},
|
||||
)
|
||||
|
||||
def test_recursiveWatch(self):
|
||||
"""
|
||||
L{inotify.INotify.watch} with recursive==True will add all the
|
||||
subdirectories under the given path to the watchlist.
|
||||
"""
|
||||
subdir = self.dirname.child("test")
|
||||
subdir2 = subdir.child("test2")
|
||||
subdir3 = subdir2.child("test3")
|
||||
subdir3.makedirs()
|
||||
dirs = [subdir, subdir2, subdir3]
|
||||
self.inotify.watch(self.dirname, recursive=True)
|
||||
# let's even call this twice so that we test that nothing breaks
|
||||
self.inotify.watch(self.dirname, recursive=True)
|
||||
for d in dirs:
|
||||
self.assertTrue(self.inotify._isWatched(d))
|
||||
|
||||
def test_connectionLostError(self):
|
||||
"""
|
||||
L{inotify.INotify.connectionLost} if there's a problem while closing
|
||||
the fd shouldn't raise the exception but should log the error
|
||||
"""
|
||||
import os
|
||||
|
||||
in_ = inotify.INotify()
|
||||
os.close(in_._fd)
|
||||
in_.loseConnection()
|
||||
self.flushLoggedErrors()
|
||||
|
||||
def test_noAutoAddSubdirectory(self):
|
||||
"""
|
||||
L{inotify.INotify.watch} with autoAdd==False will stop inotify
|
||||
from watching subdirectories created under the watched one.
|
||||
"""
|
||||
|
||||
def _callback(wp, fp, mask):
|
||||
# We are notified before we actually process new
|
||||
# directories, so we need to defer this check.
|
||||
def _():
|
||||
try:
|
||||
self.assertFalse(self.inotify._isWatched(subdir))
|
||||
d.callback(None)
|
||||
except Exception:
|
||||
d.errback()
|
||||
|
||||
reactor.callLater(0, _)
|
||||
|
||||
checkMask = inotify.IN_ISDIR | inotify.IN_CREATE
|
||||
self.inotify.watch(
|
||||
self.dirname, mask=checkMask, autoAdd=False, callbacks=[_callback]
|
||||
)
|
||||
subdir = self.dirname.child("test")
|
||||
d = defer.Deferred()
|
||||
subdir.createDirectory()
|
||||
return d
|
||||
|
||||
def test_seriesOfWatchAndIgnore(self):
|
||||
"""
|
||||
L{inotify.INotify} will watch a filepath for events even if the same
|
||||
path is repeatedly added/removed/re-added to the watchpoints.
|
||||
"""
|
||||
expectedPath = self.dirname.child("foo.bar2")
|
||||
expectedPath.touch()
|
||||
|
||||
notified = defer.Deferred()
|
||||
|
||||
def cbNotified(result):
|
||||
(ignored, filename, events) = result
|
||||
self.assertEqual(filename.asBytesMode(), expectedPath.asBytesMode())
|
||||
self.assertTrue(events & inotify.IN_DELETE_SELF)
|
||||
|
||||
def callIt(*args):
|
||||
notified.callback(args)
|
||||
|
||||
# Watch, ignore, watch again to get into the state being tested.
|
||||
self.assertTrue(self.inotify.watch(expectedPath, callbacks=[callIt]))
|
||||
self.inotify.ignore(expectedPath)
|
||||
self.assertTrue(
|
||||
self.inotify.watch(
|
||||
expectedPath, mask=inotify.IN_DELETE_SELF, callbacks=[callIt]
|
||||
)
|
||||
)
|
||||
|
||||
notified.addCallback(cbNotified)
|
||||
|
||||
# Apparently in kernel version < 2.6.25, inofify has a bug in the way
|
||||
# similar events are coalesced. So, be sure to generate a different
|
||||
# event here than the touch() at the top of this method might have
|
||||
# generated.
|
||||
expectedPath.remove()
|
||||
|
||||
return notified
|
||||
|
||||
def test_ignoreFilePath(self):
|
||||
"""
|
||||
L{inotify.INotify} will ignore a filepath after it has been removed from
|
||||
the watch list.
|
||||
"""
|
||||
expectedPath = self.dirname.child("foo.bar2")
|
||||
expectedPath.touch()
|
||||
expectedPath2 = self.dirname.child("foo.bar3")
|
||||
expectedPath2.touch()
|
||||
|
||||
notified = defer.Deferred()
|
||||
|
||||
def cbNotified(result):
|
||||
(ignored, filename, events) = result
|
||||
self.assertEqual(filename.asBytesMode(), expectedPath2.asBytesMode())
|
||||
self.assertTrue(events & inotify.IN_DELETE_SELF)
|
||||
|
||||
def callIt(*args):
|
||||
notified.callback(args)
|
||||
|
||||
self.assertTrue(
|
||||
self.inotify.watch(expectedPath, inotify.IN_DELETE_SELF, callbacks=[callIt])
|
||||
)
|
||||
notified.addCallback(cbNotified)
|
||||
|
||||
self.assertTrue(
|
||||
self.inotify.watch(
|
||||
expectedPath2, inotify.IN_DELETE_SELF, callbacks=[callIt]
|
||||
)
|
||||
)
|
||||
|
||||
self.inotify.ignore(expectedPath)
|
||||
|
||||
expectedPath.remove()
|
||||
expectedPath2.remove()
|
||||
|
||||
return notified
|
||||
|
||||
def test_ignoreNonWatchedFile(self):
|
||||
"""
|
||||
L{inotify.INotify} will raise KeyError if a non-watched filepath is
|
||||
ignored.
|
||||
"""
|
||||
expectedPath = self.dirname.child("foo.ignored")
|
||||
expectedPath.touch()
|
||||
|
||||
self.assertRaises(KeyError, self.inotify.ignore, expectedPath)
|
||||
|
||||
def test_complexSubdirectoryAutoAdd(self):
|
||||
"""
|
||||
L{inotify.INotify} with autoAdd==True for a watched path
|
||||
generates events for every file or directory already present
|
||||
in a newly created subdirectory under the watched one.
|
||||
|
||||
This tests that we solve a race condition in inotify even though
|
||||
we may generate duplicate events.
|
||||
"""
|
||||
calls = set()
|
||||
|
||||
def _callback(wp, filename, mask):
|
||||
calls.add(filename)
|
||||
if len(calls) == 6:
|
||||
try:
|
||||
self.assertTrue(self.inotify._isWatched(subdir))
|
||||
self.assertTrue(self.inotify._isWatched(subdir2))
|
||||
self.assertTrue(self.inotify._isWatched(subdir3))
|
||||
created = someFiles + [subdir, subdir2, subdir3]
|
||||
created = {f.asBytesMode() for f in created}
|
||||
self.assertEqual(len(calls), len(created))
|
||||
self.assertEqual(calls, created)
|
||||
except Exception:
|
||||
d.errback()
|
||||
else:
|
||||
d.callback(None)
|
||||
|
||||
checkMask = inotify.IN_ISDIR | inotify.IN_CREATE
|
||||
self.inotify.watch(
|
||||
self.dirname, mask=checkMask, autoAdd=True, callbacks=[_callback]
|
||||
)
|
||||
subdir = self.dirname.child("test")
|
||||
subdir2 = subdir.child("test2")
|
||||
subdir3 = subdir2.child("test3")
|
||||
d = defer.Deferred()
|
||||
subdir3.makedirs()
|
||||
|
||||
someFiles = [
|
||||
subdir.child("file1.dat"),
|
||||
subdir2.child("file2.dat"),
|
||||
subdir3.child("file3.dat"),
|
||||
]
|
||||
# Add some files in pretty much all the directories so that we
|
||||
# see that we process all of them.
|
||||
for i, filename in enumerate(someFiles):
|
||||
filename.setContent(filename.path.encode(sys.getfilesystemencoding()))
|
||||
return d
|
||||
@@ -0,0 +1,212 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.internet.iocpreactor}.
|
||||
"""
|
||||
|
||||
import errno
|
||||
import sys
|
||||
import time
|
||||
from array import array
|
||||
from socket import AF_INET, AF_INET6, SOCK_STREAM, SOL_SOCKET, socket
|
||||
from struct import pack
|
||||
from unittest import skipIf
|
||||
|
||||
from zope.interface.verify import verifyClass
|
||||
|
||||
from twisted.internet.interfaces import IPushProducer
|
||||
from twisted.python.log import msg
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
try:
|
||||
from twisted.internet.iocpreactor import iocpsupport as _iocp, tcp, udp
|
||||
from twisted.internet.iocpreactor.abstract import FileHandle
|
||||
from twisted.internet.iocpreactor.const import SO_UPDATE_ACCEPT_CONTEXT
|
||||
from twisted.internet.iocpreactor.interfaces import IReadWriteHandle
|
||||
from twisted.internet.iocpreactor.reactor import (
|
||||
EVENTS_PER_LOOP,
|
||||
KEY_NORMAL,
|
||||
IOCPReactor,
|
||||
)
|
||||
except ImportError:
|
||||
if sys.platform == "win32":
|
||||
raise
|
||||
|
||||
skip = "This test only applies to IOCPReactor"
|
||||
|
||||
try:
|
||||
socket(AF_INET6, SOCK_STREAM).close()
|
||||
except OSError as e:
|
||||
ipv6Skip = True
|
||||
ipv6SkipReason = str(e)
|
||||
|
||||
else:
|
||||
ipv6Skip = False
|
||||
ipv6SkipReason = ""
|
||||
|
||||
|
||||
class SupportTests(TestCase):
|
||||
"""
|
||||
Tests for L{twisted.internet.iocpreactor.iocpsupport}, low-level reactor
|
||||
implementation helpers.
|
||||
"""
|
||||
|
||||
def _acceptAddressTest(self, family, localhost):
|
||||
"""
|
||||
Create a C{SOCK_STREAM} connection to localhost using a socket with an
|
||||
address family of C{family} and assert that the result of
|
||||
L{iocpsupport.get_accept_addrs} is consistent with the result of
|
||||
C{socket.getsockname} and C{socket.getpeername}.
|
||||
|
||||
A port starts listening (is bound) at the low-level socket without
|
||||
calling accept() yet.
|
||||
A client is then connected.
|
||||
After the client is connected IOCP accept() is called, which is the
|
||||
target of these tests.
|
||||
|
||||
Most of the time, the socket is ready instantly, but sometimes
|
||||
the socket is not ready right away after calling IOCP accept().
|
||||
It should not take more than 5 seconds for a socket to be ready, as
|
||||
the client connection is already made over the loopback interface.
|
||||
|
||||
These are flaky tests.
|
||||
Tweak the failure rate by changing the number of retries and the
|
||||
wait/sleep between retries.
|
||||
|
||||
If you will need to update the retries to wait more than 5 seconds
|
||||
for the port to be available, then there might a bug in the code and
|
||||
not the test (or a very, very busy VM running the tests).
|
||||
"""
|
||||
msg(f"family = {family!r}")
|
||||
port = socket(family, SOCK_STREAM)
|
||||
self.addCleanup(port.close)
|
||||
port.bind(("", 0))
|
||||
port.listen(1)
|
||||
client = socket(family, SOCK_STREAM)
|
||||
self.addCleanup(client.close)
|
||||
client.setblocking(False)
|
||||
try:
|
||||
client.connect((localhost, port.getsockname()[1]))
|
||||
except OSError as e:
|
||||
self.assertIn(e.errno, (errno.EINPROGRESS, errno.EWOULDBLOCK))
|
||||
|
||||
server = socket(family, SOCK_STREAM)
|
||||
self.addCleanup(server.close)
|
||||
buff = array("B", b"\0" * 256)
|
||||
self.assertEqual(0, _iocp.accept(port.fileno(), server.fileno(), buff, None))
|
||||
|
||||
for attemptsRemaining in reversed(range(5)):
|
||||
# Calling setsockopt after _iocp.accept might fail for both IPv4
|
||||
# and IPV6 with "[Errno 10057] A request to send or receive ..."
|
||||
# This is when ERROR_IO_PENDING is returned and means that the
|
||||
# socket is not yet ready and accept will be handled via the
|
||||
# callback event.
|
||||
# For this test, with the purpose of keeping the test simple,
|
||||
# we don't implement the event callback.
|
||||
# The event callback functionality is tested via the high level
|
||||
# tests for general reactor API.
|
||||
# We retry multiple times to cover.
|
||||
try:
|
||||
server.setsockopt(
|
||||
SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT, pack("P", port.fileno())
|
||||
)
|
||||
break
|
||||
except OSError as socketError:
|
||||
# getattr is used below to make mypy happy.
|
||||
if socketError.errno != getattr(errno, "WSAENOTCONN"):
|
||||
# This is not the expected error so re-raise the error without retrying.
|
||||
raise
|
||||
|
||||
# The socket is not yet ready to accept connections,
|
||||
# setsockopt fails.
|
||||
if attemptsRemaining == 0:
|
||||
# We ran out of retries.
|
||||
raise
|
||||
|
||||
# Without a sleep here even retrying 20 times will fail.
|
||||
# This should allow other threads to execute and hopefully with the next
|
||||
# try setsockopt will succeed.
|
||||
time.sleep(0.2)
|
||||
|
||||
self.assertEqual(
|
||||
(family, client.getpeername()[:2], client.getsockname()[:2]),
|
||||
_iocp.get_accept_addrs(server.fileno(), buff),
|
||||
)
|
||||
|
||||
def test_ipv4AcceptAddress(self):
|
||||
"""
|
||||
L{iocpsupport.get_accept_addrs} returns a three-tuple of address
|
||||
information about the socket associated with the file descriptor passed
|
||||
to it. For a connection using IPv4:
|
||||
|
||||
- the first element is C{AF_INET}
|
||||
- the second element is a two-tuple of a dotted decimal notation IPv4
|
||||
address and a port number giving the peer address of the connection
|
||||
- the third element is the same type giving the host address of the
|
||||
connection
|
||||
"""
|
||||
self._acceptAddressTest(AF_INET, "127.0.0.1")
|
||||
|
||||
@skipIf(ipv6Skip, ipv6SkipReason)
|
||||
def test_ipv6AcceptAddress(self):
|
||||
"""
|
||||
Like L{test_ipv4AcceptAddress}, but for IPv6 connections.
|
||||
In this case:
|
||||
|
||||
- the first element is C{AF_INET6}
|
||||
- the second element is a two-tuple of a hexadecimal IPv6 address
|
||||
literal and a port number giving the peer address of the connection
|
||||
- the third element is the same type giving the host address of the
|
||||
connection
|
||||
"""
|
||||
self._acceptAddressTest(AF_INET6, "::1")
|
||||
|
||||
|
||||
class IOCPReactorTests(TestCase):
|
||||
def test_noPendingTimerEvents(self):
|
||||
"""
|
||||
Test reactor behavior (doIteration) when there are no pending time
|
||||
events.
|
||||
"""
|
||||
ir = IOCPReactor()
|
||||
ir.wakeUp()
|
||||
self.assertFalse(ir.doIteration(None))
|
||||
|
||||
def test_reactorInterfaces(self):
|
||||
"""
|
||||
Verify that IOCP socket-representing classes implement IReadWriteHandle
|
||||
"""
|
||||
self.assertTrue(verifyClass(IReadWriteHandle, tcp.Connection))
|
||||
self.assertTrue(verifyClass(IReadWriteHandle, udp.Port))
|
||||
|
||||
def test_fileHandleInterfaces(self):
|
||||
"""
|
||||
Verify that L{Filehandle} implements L{IPushProducer}.
|
||||
"""
|
||||
self.assertTrue(verifyClass(IPushProducer, FileHandle))
|
||||
|
||||
def test_maxEventsPerIteration(self):
|
||||
"""
|
||||
Verify that we don't lose an event when more than EVENTS_PER_LOOP
|
||||
events occur in the same reactor iteration
|
||||
"""
|
||||
|
||||
class FakeFD:
|
||||
counter = 0
|
||||
|
||||
def logPrefix(self):
|
||||
return "FakeFD"
|
||||
|
||||
def cb(self, rc, bytes, evt):
|
||||
self.counter += 1
|
||||
|
||||
ir = IOCPReactor()
|
||||
fd = FakeFD()
|
||||
event = _iocp.Event(fd.cb, fd)
|
||||
for _ in range(EVENTS_PER_LOOP + 1):
|
||||
ir.port.postEvent(0, KEY_NORMAL, event)
|
||||
ir.doIteration(None)
|
||||
self.assertEqual(fd.counter, EVENTS_PER_LOOP)
|
||||
ir.doIteration(0)
|
||||
self.assertEqual(fd.counter, EVENTS_PER_LOOP + 1)
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.internet.kqueuereactor}.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
try:
|
||||
from twisted.internet.kqreactor import KQueueReactor, _IKQueue
|
||||
|
||||
kqueueSkip = None
|
||||
except ImportError:
|
||||
kqueueSkip = "KQueue not available."
|
||||
|
||||
|
||||
def _fakeKEvent(*args: object, **kwargs: object) -> None:
|
||||
"""
|
||||
Do nothing.
|
||||
"""
|
||||
|
||||
|
||||
def makeFakeKQueue(testKQueue: object, testKEvent: object) -> _IKQueue:
|
||||
"""
|
||||
Create a fake that implements L{_IKQueue}.
|
||||
|
||||
@param testKQueue: Something that acts like L{select.kqueue}.
|
||||
@param testKEvent: Something that acts like L{select.kevent}.
|
||||
@return: An implementation of L{_IKQueue} that includes C{testKQueue} and
|
||||
C{testKEvent}.
|
||||
"""
|
||||
|
||||
@implementer(_IKQueue)
|
||||
class FakeKQueue:
|
||||
kqueue = testKQueue
|
||||
kevent = testKEvent
|
||||
|
||||
return FakeKQueue()
|
||||
|
||||
|
||||
class KQueueTests(TestCase):
|
||||
"""
|
||||
These are tests for L{KQueueReactor}'s implementation, not its real world
|
||||
behaviour. For that, look at
|
||||
L{twisted.internet.test.reactormixins.ReactorBuilder}.
|
||||
"""
|
||||
|
||||
skip = kqueueSkip
|
||||
|
||||
def test_EINTR(self) -> None:
|
||||
"""
|
||||
L{KQueueReactor} handles L{errno.EINTR} in C{doKEvent} by returning.
|
||||
"""
|
||||
|
||||
class FakeKQueue:
|
||||
"""
|
||||
A fake KQueue that raises L{errno.EINTR} when C{control} is called,
|
||||
like a real KQueue would if it was interrupted.
|
||||
"""
|
||||
|
||||
def control(self, *args: object, **kwargs: object) -> None:
|
||||
raise OSError(errno.EINTR, "Interrupted")
|
||||
|
||||
reactor = KQueueReactor(makeFakeKQueue(FakeKQueue, _fakeKEvent))
|
||||
# This should return cleanly -- should not raise the OSError we're
|
||||
# spawning, nor get upset and raise about the incomplete KQueue fake.
|
||||
reactor.doKEvent(0)
|
||||
@@ -0,0 +1,45 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.internet.main}.
|
||||
"""
|
||||
|
||||
|
||||
from twisted.internet.error import ReactorAlreadyInstalledError
|
||||
from twisted.internet.main import installReactor
|
||||
from twisted.internet.test.modulehelpers import NoReactor
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class InstallReactorTests(unittest.SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{installReactor}.
|
||||
"""
|
||||
|
||||
def test_installReactor(self) -> None:
|
||||
"""
|
||||
L{installReactor} installs a new reactor if none is present.
|
||||
"""
|
||||
with NoReactor():
|
||||
newReactor = object()
|
||||
installReactor(newReactor)
|
||||
from twisted.internet import reactor
|
||||
|
||||
self.assertIs(newReactor, reactor)
|
||||
|
||||
def test_alreadyInstalled(self) -> None:
|
||||
"""
|
||||
If a reactor is already installed, L{installReactor} raises
|
||||
L{ReactorAlreadyInstalledError}.
|
||||
"""
|
||||
with NoReactor():
|
||||
installReactor(object())
|
||||
self.assertRaises(ReactorAlreadyInstalledError, installReactor, object())
|
||||
|
||||
def test_errorIsAnAssertionError(self) -> None:
|
||||
"""
|
||||
For backwards compatibility, L{ReactorAlreadyInstalledError} is an
|
||||
L{AssertionError}.
|
||||
"""
|
||||
self.assertTrue(issubclass(ReactorAlreadyInstalledError, AssertionError))
|
||||
@@ -0,0 +1,202 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.internet._newtls}.
|
||||
"""
|
||||
|
||||
|
||||
from twisted.internet import interfaces
|
||||
from twisted.internet.test.connectionmixins import (
|
||||
ConnectableProtocol,
|
||||
runProtocolsWithReactor,
|
||||
)
|
||||
from twisted.internet.test.reactormixins import ReactorBuilder
|
||||
from twisted.internet.test.test_tcp import TCPCreator
|
||||
from twisted.internet.test.test_tls import (
|
||||
ContextGeneratingMixin,
|
||||
SSLCreator,
|
||||
StartTLSClientCreator,
|
||||
TLSMixin,
|
||||
)
|
||||
from twisted.trial import unittest
|
||||
|
||||
try:
|
||||
from twisted.internet import _newtls as __newtls
|
||||
from twisted.protocols import tls
|
||||
except ImportError:
|
||||
_newtls = None
|
||||
else:
|
||||
_newtls = __newtls
|
||||
from zope.interface import implementer
|
||||
|
||||
|
||||
class BypassTLSTests(unittest.TestCase):
|
||||
"""
|
||||
Tests for the L{_newtls._BypassTLS} class.
|
||||
"""
|
||||
|
||||
if not _newtls:
|
||||
skip = "Couldn't import _newtls, perhaps pyOpenSSL is old or missing"
|
||||
|
||||
def test_loseConnectionPassThrough(self):
|
||||
"""
|
||||
C{_BypassTLS.loseConnection} calls C{loseConnection} on the base
|
||||
class, while preserving any default argument in the base class'
|
||||
C{loseConnection} implementation.
|
||||
"""
|
||||
default = object()
|
||||
result = []
|
||||
|
||||
class FakeTransport:
|
||||
def loseConnection(self, _connDone=default):
|
||||
result.append(_connDone)
|
||||
|
||||
bypass = _newtls._BypassTLS(FakeTransport, FakeTransport())
|
||||
|
||||
# The default from FakeTransport is used:
|
||||
bypass.loseConnection()
|
||||
self.assertEqual(result, [default])
|
||||
|
||||
# And we can pass our own:
|
||||
notDefault = object()
|
||||
bypass.loseConnection(notDefault)
|
||||
self.assertEqual(result, [default, notDefault])
|
||||
|
||||
|
||||
class FakeProducer:
|
||||
"""
|
||||
A producer that does nothing.
|
||||
"""
|
||||
|
||||
def pauseProducing(self):
|
||||
pass
|
||||
|
||||
def resumeProducing(self):
|
||||
pass
|
||||
|
||||
def stopProducing(self):
|
||||
pass
|
||||
|
||||
|
||||
@implementer(interfaces.IHandshakeListener)
|
||||
class ProducerProtocol(ConnectableProtocol):
|
||||
"""
|
||||
Register a producer, unregister it, and verify the producer hooks up to
|
||||
innards of C{TLSMemoryBIOProtocol}.
|
||||
"""
|
||||
|
||||
def __init__(self, producer, result):
|
||||
self.producer = producer
|
||||
self.result = result
|
||||
|
||||
def handshakeCompleted(self):
|
||||
if not isinstance(self.transport.protocol, tls.BufferingTLSTransport):
|
||||
# Either the test or the code have a bug...
|
||||
raise RuntimeError("TLSMemoryBIOProtocol not hooked up.")
|
||||
|
||||
self.transport.registerProducer(self.producer, True)
|
||||
# The producer was registered with the TLSMemoryBIOProtocol:
|
||||
self.result.append(self.transport.protocol._producer._producer)
|
||||
|
||||
self.transport.unregisterProducer()
|
||||
# The producer was unregistered from the TLSMemoryBIOProtocol:
|
||||
self.result.append(self.transport.protocol._producer)
|
||||
self.transport.loseConnection()
|
||||
|
||||
|
||||
class ProducerTestsMixin(ReactorBuilder, TLSMixin, ContextGeneratingMixin):
|
||||
"""
|
||||
Test the new TLS code integrates C{TLSMemoryBIOProtocol} correctly.
|
||||
"""
|
||||
|
||||
if not _newtls:
|
||||
skip = "Could not import twisted.internet._newtls"
|
||||
|
||||
def test_producerSSLFromStart(self):
|
||||
"""
|
||||
C{registerProducer} and C{unregisterProducer} on TLS transports
|
||||
created as SSL from the get go are passed to the
|
||||
C{TLSMemoryBIOProtocol}, not the underlying transport directly.
|
||||
"""
|
||||
result = []
|
||||
producer = FakeProducer()
|
||||
|
||||
runProtocolsWithReactor(
|
||||
self,
|
||||
ConnectableProtocol(),
|
||||
ProducerProtocol(producer, result),
|
||||
SSLCreator(),
|
||||
)
|
||||
self.assertEqual(result, [producer, None])
|
||||
|
||||
def test_producerAfterStartTLS(self):
|
||||
"""
|
||||
C{registerProducer} and C{unregisterProducer} on TLS transports
|
||||
created by C{startTLS} are passed to the C{TLSMemoryBIOProtocol}, not
|
||||
the underlying transport directly.
|
||||
"""
|
||||
result = []
|
||||
producer = FakeProducer()
|
||||
|
||||
runProtocolsWithReactor(
|
||||
self,
|
||||
ConnectableProtocol(),
|
||||
ProducerProtocol(producer, result),
|
||||
StartTLSClientCreator(),
|
||||
)
|
||||
self.assertEqual(result, [producer, None])
|
||||
|
||||
def startTLSAfterRegisterProducer(self, streaming):
|
||||
"""
|
||||
When a producer is registered, and then startTLS is called,
|
||||
the producer is re-registered with the C{TLSMemoryBIOProtocol}.
|
||||
"""
|
||||
clientContext = self.getClientContext()
|
||||
serverContext = self.getServerContext()
|
||||
result = []
|
||||
producer = FakeProducer()
|
||||
|
||||
class RegisterTLSProtocol(ConnectableProtocol):
|
||||
def connectionMade(self):
|
||||
self.transport.registerProducer(producer, streaming)
|
||||
self.transport.startTLS(serverContext)
|
||||
# Store TLSMemoryBIOProtocol and underlying transport producer
|
||||
# status:
|
||||
if streaming:
|
||||
# _ProducerMembrane -> producer:
|
||||
result.append(self.transport.protocol._producer._producer)
|
||||
result.append(self.transport.producer._producer)
|
||||
else:
|
||||
# _ProducerMembrane -> _PullToPush -> producer:
|
||||
result.append(self.transport.protocol._producer._producer._producer)
|
||||
result.append(self.transport.producer._producer._producer)
|
||||
self.transport.unregisterProducer()
|
||||
self.transport.loseConnection()
|
||||
|
||||
class StartTLSProtocol(ConnectableProtocol):
|
||||
def connectionMade(self):
|
||||
self.transport.startTLS(clientContext)
|
||||
|
||||
runProtocolsWithReactor(
|
||||
self, RegisterTLSProtocol(), StartTLSProtocol(), TCPCreator()
|
||||
)
|
||||
self.assertEqual(result, [producer, producer])
|
||||
|
||||
def test_startTLSAfterRegisterProducerStreaming(self):
|
||||
"""
|
||||
When a streaming producer is registered, and then startTLS is called,
|
||||
the producer is re-registered with the C{TLSMemoryBIOProtocol}.
|
||||
"""
|
||||
self.startTLSAfterRegisterProducer(True)
|
||||
|
||||
def test_startTLSAfterRegisterProducerNonStreaming(self):
|
||||
"""
|
||||
When a non-streaming producer is registered, and then startTLS is
|
||||
called, the producer is re-registered with the
|
||||
C{TLSMemoryBIOProtocol}.
|
||||
"""
|
||||
self.startTLSAfterRegisterProducer(False)
|
||||
|
||||
|
||||
globals().update(ProducerTestsMixin.makeTestCaseClasses())
|
||||
@@ -0,0 +1,41 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.internet._pollingfile}.
|
||||
"""
|
||||
|
||||
from unittest import skipIf
|
||||
|
||||
from twisted.python.runtime import platform
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
if platform.isWindows():
|
||||
from twisted.internet import _pollingfile
|
||||
else:
|
||||
_pollingfile = None # type: ignore[assignment]
|
||||
|
||||
|
||||
@skipIf(_pollingfile is None, "Test will run only on Windows.")
|
||||
class PollableWritePipeTests(TestCase):
|
||||
"""
|
||||
Tests for L{_pollingfile._PollableWritePipe}.
|
||||
"""
|
||||
|
||||
def test_writeUnicode(self) -> None:
|
||||
"""
|
||||
L{_pollingfile._PollableWritePipe.write} raises a C{TypeError} if an
|
||||
attempt is made to append unicode data to the output buffer.
|
||||
"""
|
||||
p = _pollingfile._PollableWritePipe(1, lambda: None)
|
||||
self.assertRaises(TypeError, p.write, "test")
|
||||
|
||||
def test_writeSequenceUnicode(self) -> None:
|
||||
"""
|
||||
L{_pollingfile._PollableWritePipe.writeSequence} raises a C{TypeError}
|
||||
if unicode data is part of the data sequence to be appended to the
|
||||
output buffer.
|
||||
"""
|
||||
p = _pollingfile._PollableWritePipe(1, lambda: None)
|
||||
self.assertRaises(TypeError, p.writeSequence, ["test"])
|
||||
self.assertRaises(TypeError, p.writeSequence, ("test",))
|
||||
@@ -0,0 +1,348 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.internet.posixbase} and supporting code.
|
||||
"""
|
||||
import os
|
||||
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.internet.interfaces import IReadDescriptor
|
||||
from twisted.internet.posixbase import PosixReactorBase, _Waker
|
||||
from twisted.internet.protocol import ServerFactory
|
||||
from twisted.python.runtime import platform
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
skipSockets = None
|
||||
try:
|
||||
from twisted.internet import unix
|
||||
from twisted.test.test_unix import ClientProto
|
||||
except ImportError:
|
||||
skipSockets = "Platform does not support AF_UNIX sockets"
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.tcp import Port
|
||||
|
||||
|
||||
class WarningCheckerTestCase(TestCase):
|
||||
"""
|
||||
A test case that will make sure that no warnings are left unchecked at the end of a test run.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# FIXME:
|
||||
# https://twistedmatrix.com/trac/ticket/10332
|
||||
# For now, try to start each test without previous warnings
|
||||
# on Windows CI environment.
|
||||
# We still want to see failures on local Windows development environment to make it easier to fix them,
|
||||
# rather than ignoring the errors.
|
||||
if os.environ.get("CI", "").lower() == "true" and platform.isWindows():
|
||||
self.flushWarnings()
|
||||
|
||||
def tearDown(self):
|
||||
try:
|
||||
super().tearDown()
|
||||
finally:
|
||||
warnings = self.flushWarnings()
|
||||
if os.environ.get("CI", "").lower() == "true" and platform.isWindows():
|
||||
# FIXME:
|
||||
# https://twistedmatrix.com/trac/ticket/10332
|
||||
# For now don't raise errors on Windows as the existing tests are dirty and we don't have the dev resources to fix this.
|
||||
# If you care about Twisted on Windows, enable this check and hunt for the test that is generating the warnings.
|
||||
# Note that even with this check disabled, you can still see flaky tests on Windows, as due to stray delayed calls
|
||||
# the warnings can be generated while another test is running.
|
||||
return
|
||||
self.assertEqual(
|
||||
len(warnings), 0, f"Warnings found at the end of the test:\n{warnings}"
|
||||
)
|
||||
|
||||
|
||||
class TrivialReactor(PosixReactorBase):
|
||||
def __init__(self):
|
||||
self._readers = {}
|
||||
self._writers = {}
|
||||
PosixReactorBase.__init__(self)
|
||||
|
||||
def addReader(self, reader):
|
||||
self._readers[reader] = True
|
||||
|
||||
def removeReader(self, reader):
|
||||
del self._readers[reader]
|
||||
|
||||
def addWriter(self, writer):
|
||||
self._writers[writer] = True
|
||||
|
||||
def removeWriter(self, writer):
|
||||
del self._writers[writer]
|
||||
|
||||
|
||||
class PosixReactorBaseTests(WarningCheckerTestCase):
|
||||
"""
|
||||
Tests for L{PosixReactorBase}.
|
||||
"""
|
||||
|
||||
def _checkWaker(self, reactor):
|
||||
self.assertIsInstance(reactor.waker, _Waker)
|
||||
self.assertIn(reactor.waker, reactor._internalReaders)
|
||||
self.assertIn(reactor.waker, reactor._readers)
|
||||
|
||||
def test_wakerIsInternalReader(self):
|
||||
"""
|
||||
When L{PosixReactorBase} is instantiated, it creates a waker and adds
|
||||
it to its internal readers set.
|
||||
"""
|
||||
reactor = TrivialReactor()
|
||||
self._checkWaker(reactor)
|
||||
|
||||
def test_removeAllSkipsInternalReaders(self):
|
||||
"""
|
||||
Any L{IReadDescriptor}s in L{PosixReactorBase._internalReaders} are
|
||||
left alone by L{PosixReactorBase._removeAll}.
|
||||
"""
|
||||
reactor = TrivialReactor()
|
||||
extra = object()
|
||||
reactor._internalReaders.add(extra)
|
||||
reactor.addReader(extra)
|
||||
reactor._removeAll(reactor._readers, reactor._writers)
|
||||
self._checkWaker(reactor)
|
||||
self.assertIn(extra, reactor._internalReaders)
|
||||
self.assertIn(extra, reactor._readers)
|
||||
|
||||
def test_removeAllReturnsRemovedDescriptors(self):
|
||||
"""
|
||||
L{PosixReactorBase._removeAll} returns a list of removed
|
||||
L{IReadDescriptor} and L{IWriteDescriptor} objects.
|
||||
"""
|
||||
reactor = TrivialReactor()
|
||||
reader = object()
|
||||
writer = object()
|
||||
reactor.addReader(reader)
|
||||
reactor.addWriter(writer)
|
||||
removed = reactor._removeAll(reactor._readers, reactor._writers)
|
||||
self.assertEqual(set(removed), {reader, writer})
|
||||
self.assertNotIn(reader, reactor._readers)
|
||||
self.assertNotIn(writer, reactor._writers)
|
||||
|
||||
|
||||
class TCPPortTests(WarningCheckerTestCase):
|
||||
"""
|
||||
Tests for L{twisted.internet.tcp.Port}.
|
||||
"""
|
||||
|
||||
if not isinstance(reactor, PosixReactorBase):
|
||||
skip = "Non-posixbase reactor"
|
||||
|
||||
def test_connectionLostFailed(self):
|
||||
"""
|
||||
L{Port.stopListening} returns a L{Deferred} which errbacks if
|
||||
L{Port.connectionLost} raises an exception.
|
||||
"""
|
||||
port = Port(12345, ServerFactory())
|
||||
port.connected = True
|
||||
port.connectionLost = lambda reason: 1 // 0
|
||||
return self.assertFailure(port.stopListening(), ZeroDivisionError)
|
||||
|
||||
|
||||
class TimeoutReportReactor(PosixReactorBase):
|
||||
"""
|
||||
A reactor which is just barely runnable and which cannot monitor any
|
||||
readers or writers, and which fires a L{Deferred} with the timeout
|
||||
passed to its C{doIteration} method as soon as that method is invoked.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
PosixReactorBase.__init__(self)
|
||||
self.iterationTimeout = Deferred()
|
||||
self.now = 100
|
||||
|
||||
def addReader(self, reader: IReadDescriptor) -> None:
|
||||
"""
|
||||
Ignore the reader. This is necessary because the waker will be
|
||||
added. However, we won't actually monitor it for any events.
|
||||
"""
|
||||
|
||||
def removeReader(self, reader: IReadDescriptor) -> None:
|
||||
"""
|
||||
See L{addReader}.
|
||||
"""
|
||||
|
||||
def removeAll(self):
|
||||
"""
|
||||
There are no readers or writers, so there is nothing to remove.
|
||||
This will be called when the reactor stops, though, so it must be
|
||||
implemented.
|
||||
"""
|
||||
return []
|
||||
|
||||
def seconds(self):
|
||||
"""
|
||||
Override the real clock with a deterministic one that can be easily
|
||||
controlled in a unit test.
|
||||
"""
|
||||
return self.now
|
||||
|
||||
def doIteration(self, timeout):
|
||||
d = self.iterationTimeout
|
||||
if d is not None:
|
||||
self.iterationTimeout = None
|
||||
d.callback(timeout)
|
||||
|
||||
|
||||
class IterationTimeoutTests(WarningCheckerTestCase):
|
||||
"""
|
||||
Tests for the timeout argument L{PosixReactorBase.run} calls
|
||||
L{PosixReactorBase.doIteration} with in the presence of various delayed
|
||||
calls.
|
||||
"""
|
||||
|
||||
def _checkIterationTimeout(self, reactor):
|
||||
timeout = []
|
||||
reactor.iterationTimeout.addCallback(timeout.append)
|
||||
reactor.iterationTimeout.addCallback(lambda ignored: reactor.stop())
|
||||
reactor.run()
|
||||
return timeout[0]
|
||||
|
||||
def test_noCalls(self):
|
||||
"""
|
||||
If there are no delayed calls, C{doIteration} is called with a
|
||||
timeout of L{None}.
|
||||
"""
|
||||
reactor = TimeoutReportReactor()
|
||||
timeout = self._checkIterationTimeout(reactor)
|
||||
self.assertIsNone(timeout)
|
||||
|
||||
def test_delayedCall(self):
|
||||
"""
|
||||
If there is a delayed call, C{doIteration} is called with a timeout
|
||||
which is the difference between the current time and the time at
|
||||
which that call is to run.
|
||||
"""
|
||||
reactor = TimeoutReportReactor()
|
||||
reactor.callLater(100, lambda: None)
|
||||
timeout = self._checkIterationTimeout(reactor)
|
||||
self.assertEqual(timeout, 100)
|
||||
|
||||
def test_timePasses(self):
|
||||
"""
|
||||
If a delayed call is scheduled and then some time passes, the
|
||||
timeout passed to C{doIteration} is reduced by the amount of time
|
||||
which passed.
|
||||
"""
|
||||
reactor = TimeoutReportReactor()
|
||||
reactor.callLater(100, lambda: None)
|
||||
reactor.now += 25
|
||||
timeout = self._checkIterationTimeout(reactor)
|
||||
self.assertEqual(timeout, 75)
|
||||
|
||||
def test_multipleDelayedCalls(self):
|
||||
"""
|
||||
If there are several delayed calls, C{doIteration} is called with a
|
||||
timeout which is the difference between the current time and the
|
||||
time at which the earlier of the two calls is to run.
|
||||
"""
|
||||
reactor = TimeoutReportReactor()
|
||||
reactor.callLater(50, lambda: None)
|
||||
reactor.callLater(10, lambda: None)
|
||||
reactor.callLater(100, lambda: None)
|
||||
timeout = self._checkIterationTimeout(reactor)
|
||||
self.assertEqual(timeout, 10)
|
||||
|
||||
def test_resetDelayedCall(self):
|
||||
"""
|
||||
If a delayed call is reset, the timeout passed to C{doIteration} is
|
||||
based on the interval between the time when reset is called and the
|
||||
new delay of the call.
|
||||
"""
|
||||
reactor = TimeoutReportReactor()
|
||||
call = reactor.callLater(50, lambda: None)
|
||||
reactor.now += 25
|
||||
call.reset(15)
|
||||
timeout = self._checkIterationTimeout(reactor)
|
||||
self.assertEqual(timeout, 15)
|
||||
|
||||
def test_delayDelayedCall(self):
|
||||
"""
|
||||
If a delayed call is re-delayed, the timeout passed to
|
||||
C{doIteration} is based on the remaining time before the call would
|
||||
have been made and the additional amount of time passed to the delay
|
||||
method.
|
||||
"""
|
||||
reactor = TimeoutReportReactor()
|
||||
call = reactor.callLater(50, lambda: None)
|
||||
reactor.now += 10
|
||||
call.delay(20)
|
||||
timeout = self._checkIterationTimeout(reactor)
|
||||
self.assertEqual(timeout, 60)
|
||||
|
||||
def test_cancelDelayedCall(self):
|
||||
"""
|
||||
If the only delayed call is canceled, L{None} is the timeout passed
|
||||
to C{doIteration}.
|
||||
"""
|
||||
reactor = TimeoutReportReactor()
|
||||
call = reactor.callLater(50, lambda: None)
|
||||
call.cancel()
|
||||
timeout = self._checkIterationTimeout(reactor)
|
||||
self.assertIsNone(timeout)
|
||||
|
||||
|
||||
class ConnectedDatagramPortTests(WarningCheckerTestCase):
|
||||
"""
|
||||
Test connected datagram UNIX sockets.
|
||||
"""
|
||||
|
||||
if skipSockets is not None:
|
||||
skip = skipSockets
|
||||
|
||||
def test_connectionFailedDoesntCallLoseConnection(self):
|
||||
"""
|
||||
L{ConnectedDatagramPort} does not call the deprecated C{loseConnection}
|
||||
in L{ConnectedDatagramPort.connectionFailed}.
|
||||
"""
|
||||
|
||||
def loseConnection():
|
||||
"""
|
||||
Dummy C{loseConnection} method. C{loseConnection} is deprecated and
|
||||
should not get called.
|
||||
"""
|
||||
self.fail("loseConnection is deprecated and should not get called.")
|
||||
|
||||
port = unix.ConnectedDatagramPort(None, ClientProto())
|
||||
port.loseConnection = loseConnection
|
||||
port.connectionFailed("goodbye")
|
||||
|
||||
def test_connectionFailedCallsStopListening(self):
|
||||
"""
|
||||
L{ConnectedDatagramPort} calls L{ConnectedDatagramPort.stopListening}
|
||||
instead of the deprecated C{loseConnection} in
|
||||
L{ConnectedDatagramPort.connectionFailed}.
|
||||
"""
|
||||
self.called = False
|
||||
|
||||
def stopListening():
|
||||
"""
|
||||
Dummy C{stopListening} method.
|
||||
"""
|
||||
self.called = True
|
||||
|
||||
port = unix.ConnectedDatagramPort(None, ClientProto())
|
||||
port.stopListening = stopListening
|
||||
port.connectionFailed("goodbye")
|
||||
self.assertTrue(self.called)
|
||||
|
||||
|
||||
class WakerTests(WarningCheckerTestCase):
|
||||
def test_noWakerConstructionWarnings(self):
|
||||
"""
|
||||
No warnings are generated when constructing the waker.
|
||||
"""
|
||||
waker = _Waker()
|
||||
|
||||
warnings = self.flushWarnings()
|
||||
self.assertEqual(len(warnings), 0, warnings)
|
||||
|
||||
# Explicitly close the waker to leave a clean state at the end of the test.
|
||||
waker.connectionLost(None)
|
||||
warnings = self.flushWarnings()
|
||||
self.assertEqual(len(warnings), 0, warnings)
|
||||
@@ -0,0 +1,349 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for POSIX-based L{IReactorProcess} implementations.
|
||||
"""
|
||||
|
||||
|
||||
import errno
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
platformSkip: Optional[str]
|
||||
try:
|
||||
import fcntl
|
||||
except ImportError:
|
||||
platformSkip = "non-POSIX platform"
|
||||
else:
|
||||
from twisted.internet import process
|
||||
|
||||
platformSkip = None
|
||||
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class FakeFile:
|
||||
"""
|
||||
A dummy file object which records when it is closed.
|
||||
"""
|
||||
|
||||
def __init__(self, testcase, fd):
|
||||
self.testcase = testcase
|
||||
self.fd = fd
|
||||
|
||||
def close(self):
|
||||
self.testcase._files.remove(self.fd)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
|
||||
|
||||
class FakeResourceModule:
|
||||
"""
|
||||
Fake version of L{resource} which hard-codes a particular rlimit for maximum
|
||||
open files.
|
||||
|
||||
@ivar _limit: The value to return for the hard limit of number of open files.
|
||||
"""
|
||||
|
||||
RLIMIT_NOFILE = 1
|
||||
|
||||
def __init__(self, limit):
|
||||
self._limit = limit
|
||||
|
||||
def getrlimit(self, no):
|
||||
"""
|
||||
A fake of L{resource.getrlimit} which returns a pre-determined result.
|
||||
"""
|
||||
if no == self.RLIMIT_NOFILE:
|
||||
return [0, self._limit]
|
||||
return [123, 456]
|
||||
|
||||
|
||||
class FDDetectorTests(TestCase):
|
||||
"""
|
||||
Tests for _FDDetector class in twisted.internet.process, which detects
|
||||
which function to drop in place for the _listOpenFDs method.
|
||||
|
||||
@ivar devfs: A flag indicating whether the filesystem fake will indicate
|
||||
that /dev/fd exists.
|
||||
|
||||
@ivar accurateDevFDResults: A flag indicating whether the /dev/fd fake
|
||||
returns accurate open file information.
|
||||
|
||||
@ivar procfs: A flag indicating whether the filesystem fake will indicate
|
||||
that /proc/<pid>/fd exists.
|
||||
"""
|
||||
|
||||
skip = platformSkip
|
||||
|
||||
devfs = False
|
||||
accurateDevFDResults = False
|
||||
|
||||
procfs = False
|
||||
|
||||
def getpid(self):
|
||||
"""
|
||||
Fake os.getpid, always return the same thing
|
||||
"""
|
||||
return 123
|
||||
|
||||
def listdir(self, arg):
|
||||
"""
|
||||
Fake os.listdir, depending on what mode we're in to simulate behaviour.
|
||||
|
||||
@param arg: the directory to list
|
||||
"""
|
||||
accurate = map(str, self._files)
|
||||
if self.procfs and arg == ("/proc/%d/fd" % (self.getpid(),)):
|
||||
return accurate
|
||||
if self.devfs and arg == "/dev/fd":
|
||||
if self.accurateDevFDResults:
|
||||
return accurate
|
||||
return ["0", "1", "2"]
|
||||
raise OSError()
|
||||
|
||||
def openfile(self, fname, mode):
|
||||
"""
|
||||
This is a mock for L{open}. It keeps track of opened files so extra
|
||||
descriptors can be returned from the mock for L{os.listdir} when used on
|
||||
one of the list-of-filedescriptors directories.
|
||||
|
||||
A L{FakeFile} is returned which can be closed to remove the new
|
||||
descriptor from the open list.
|
||||
"""
|
||||
# Find the smallest unused file descriptor and give it to the new file.
|
||||
f = FakeFile(self, min(set(range(1024)) - set(self._files)))
|
||||
self._files.append(f.fd)
|
||||
return f
|
||||
|
||||
def hideResourceModule(self):
|
||||
"""
|
||||
Make the L{resource} module unimportable for the remainder of the
|
||||
current test method.
|
||||
"""
|
||||
sys.modules["resource"] = None
|
||||
|
||||
def revealResourceModule(self, limit):
|
||||
"""
|
||||
Make a L{FakeResourceModule} instance importable at the L{resource}
|
||||
name.
|
||||
|
||||
@param limit: The value which will be returned for the hard limit of
|
||||
number of open files by the fake resource module's C{getrlimit}
|
||||
function.
|
||||
"""
|
||||
sys.modules["resource"] = FakeResourceModule(limit)
|
||||
|
||||
def replaceResourceModule(self, value):
|
||||
"""
|
||||
Restore the original resource module to L{sys.modules}.
|
||||
"""
|
||||
if value is None:
|
||||
try:
|
||||
del sys.modules["resource"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
sys.modules["resource"] = value
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up the tests, giving ourselves a detector object to play with and
|
||||
setting up its testable knobs to refer to our mocked versions.
|
||||
"""
|
||||
self.detector = process._FDDetector()
|
||||
self.detector.listdir = self.listdir
|
||||
self.detector.getpid = self.getpid
|
||||
self.detector.openfile = self.openfile
|
||||
self._files = [0, 1, 2]
|
||||
self.addCleanup(self.replaceResourceModule, sys.modules.get("resource"))
|
||||
|
||||
def test_selectFirstWorking(self):
|
||||
"""
|
||||
L{FDDetector._getImplementation} returns the first method from its
|
||||
C{_implementations} list which returns results which reflect a newly
|
||||
opened file descriptor.
|
||||
"""
|
||||
|
||||
def failWithException():
|
||||
raise ValueError("This does not work")
|
||||
|
||||
def failWithWrongResults():
|
||||
return [0, 1, 2]
|
||||
|
||||
def correct():
|
||||
return self._files[:]
|
||||
|
||||
self.detector._implementations = [
|
||||
failWithException,
|
||||
failWithWrongResults,
|
||||
correct,
|
||||
]
|
||||
|
||||
self.assertIs(correct, self.detector._getImplementation())
|
||||
|
||||
def test_selectLast(self):
|
||||
"""
|
||||
L{FDDetector._getImplementation} returns the last method from its
|
||||
C{_implementations} list if none of the implementations manage to return
|
||||
results which reflect a newly opened file descriptor.
|
||||
"""
|
||||
|
||||
def failWithWrongResults():
|
||||
return [3, 5, 9]
|
||||
|
||||
def failWithOtherWrongResults():
|
||||
return [0, 1, 2]
|
||||
|
||||
self.detector._implementations = [
|
||||
failWithWrongResults,
|
||||
failWithOtherWrongResults,
|
||||
]
|
||||
|
||||
self.assertIs(failWithOtherWrongResults, self.detector._getImplementation())
|
||||
|
||||
def test_identityOfListOpenFDsChanges(self):
|
||||
"""
|
||||
Check that the identity of _listOpenFDs changes after running
|
||||
_listOpenFDs the first time, but not after the second time it's run.
|
||||
|
||||
In other words, check that the monkey patching actually works.
|
||||
"""
|
||||
# Create a new instance
|
||||
detector = process._FDDetector()
|
||||
|
||||
first = detector._listOpenFDs.__name__
|
||||
detector._listOpenFDs()
|
||||
second = detector._listOpenFDs.__name__
|
||||
detector._listOpenFDs()
|
||||
third = detector._listOpenFDs.__name__
|
||||
|
||||
self.assertNotEqual(first, second)
|
||||
self.assertEqual(second, third)
|
||||
|
||||
def test_devFDImplementation(self):
|
||||
"""
|
||||
L{_FDDetector._devFDImplementation} raises L{OSError} if there is no
|
||||
I{/dev/fd} directory, otherwise it returns the basenames of its children
|
||||
interpreted as integers.
|
||||
"""
|
||||
self.devfs = False
|
||||
self.assertRaises(OSError, self.detector._devFDImplementation)
|
||||
self.devfs = True
|
||||
self.accurateDevFDResults = False
|
||||
self.assertEqual([0, 1, 2], self.detector._devFDImplementation())
|
||||
|
||||
def test_procFDImplementation(self):
|
||||
"""
|
||||
L{_FDDetector._procFDImplementation} raises L{OSError} if there is no
|
||||
I{/proc/<pid>/fd} directory, otherwise it returns the basenames of its
|
||||
children interpreted as integers.
|
||||
"""
|
||||
self.procfs = False
|
||||
self.assertRaises(OSError, self.detector._procFDImplementation)
|
||||
self.procfs = True
|
||||
self.assertEqual([0, 1, 2], self.detector._procFDImplementation())
|
||||
|
||||
def test_resourceFDImplementation(self):
|
||||
"""
|
||||
L{_FDDetector._fallbackFDImplementation} uses the L{resource} module if
|
||||
it is available, returning a range of integers from 0 to the
|
||||
minimum of C{1024} and the hard I{NOFILE} limit.
|
||||
"""
|
||||
# When the resource module is here, use its value.
|
||||
self.revealResourceModule(512)
|
||||
self.assertEqual(
|
||||
list(range(512)), list(self.detector._fallbackFDImplementation())
|
||||
)
|
||||
|
||||
# But limit its value to the arbitrarily selected value 1024.
|
||||
self.revealResourceModule(2048)
|
||||
self.assertEqual(
|
||||
list(range(1024)), list(self.detector._fallbackFDImplementation())
|
||||
)
|
||||
|
||||
def test_fallbackFDImplementation(self):
|
||||
"""
|
||||
L{_FDDetector._fallbackFDImplementation}, the implementation of last
|
||||
resort, succeeds with a fixed range of integers from 0 to 1024 when the
|
||||
L{resource} module is not importable.
|
||||
"""
|
||||
self.hideResourceModule()
|
||||
self.assertEqual(
|
||||
list(range(1024)), list(self.detector._fallbackFDImplementation())
|
||||
)
|
||||
|
||||
|
||||
class FileDescriptorTests(TestCase):
|
||||
"""
|
||||
Tests for L{twisted.internet.process._listOpenFDs}
|
||||
"""
|
||||
|
||||
skip = platformSkip
|
||||
|
||||
def test_openFDs(self):
|
||||
"""
|
||||
File descriptors returned by L{_listOpenFDs} are mostly open.
|
||||
|
||||
This test assumes that zero-legth writes fail with EBADF on closed
|
||||
file descriptors.
|
||||
"""
|
||||
for fd in process._listOpenFDs():
|
||||
try:
|
||||
fcntl.fcntl(fd, fcntl.F_GETFL)
|
||||
except OSError as err:
|
||||
self.assertEqual(
|
||||
errno.EBADF,
|
||||
err.errno,
|
||||
"fcntl(%d, F_GETFL) failed with unexpected errno %d"
|
||||
% (fd, err.errno),
|
||||
)
|
||||
|
||||
def test_expectedFDs(self):
|
||||
"""
|
||||
L{_listOpenFDs} lists expected file descriptors.
|
||||
"""
|
||||
# This is a tricky test. A priori, there is no way to know what file
|
||||
# descriptors are open now, so there is no way to know what _listOpenFDs
|
||||
# should return. Work around this by creating some new file descriptors
|
||||
# which we can know the state of and then just making assertions about
|
||||
# their presence or absence in the result.
|
||||
|
||||
# Expect a file we just opened to be listed.
|
||||
f = open(os.devnull)
|
||||
openfds = process._listOpenFDs()
|
||||
self.assertIn(f.fileno(), openfds)
|
||||
|
||||
# Expect a file we just closed not to be listed - with a caveat. The
|
||||
# implementation may need to open a file to discover the result. That
|
||||
# open file descriptor will be allocated the same number as the one we
|
||||
# just closed. So, instead, create a hole in the file descriptor space
|
||||
# to catch that internal descriptor and make the assertion about a
|
||||
# different closed file descriptor.
|
||||
|
||||
# This gets allocated a file descriptor larger than f's, since nothing
|
||||
# has been closed since we opened f.
|
||||
fd = os.dup(f.fileno())
|
||||
|
||||
# But sanity check that; if it fails the test is invalid.
|
||||
self.assertTrue(
|
||||
fd > f.fileno(),
|
||||
"Expected duplicate file descriptor to be greater than original",
|
||||
)
|
||||
|
||||
try:
|
||||
# Get rid of the original, creating the hole. The copy should still
|
||||
# be open, of course.
|
||||
f.close()
|
||||
self.assertIn(fd, process._listOpenFDs())
|
||||
finally:
|
||||
# Get rid of the copy now
|
||||
os.close(fd)
|
||||
# And it should not appear in the result.
|
||||
self.assertNotIn(fd, process._listOpenFDs())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,545 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.internet.protocol}.
|
||||
"""
|
||||
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
from zope.interface import implementer
|
||||
from zope.interface.verify import verifyObject
|
||||
|
||||
from twisted.internet.defer import CancelledError
|
||||
from twisted.internet.interfaces import (
|
||||
IConsumer,
|
||||
ILoggingContext,
|
||||
IProtocol,
|
||||
IProtocolFactory,
|
||||
)
|
||||
from twisted.internet.protocol import (
|
||||
ClientCreator,
|
||||
ConsumerToProtocolAdapter,
|
||||
Factory,
|
||||
FileWrapper,
|
||||
Protocol,
|
||||
ProtocolToConsumerAdapter,
|
||||
)
|
||||
from twisted.internet.testing import MemoryReactorClock, StringTransport
|
||||
from twisted.logger import LogLevel, globalLogPublisher
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class ClientCreatorTests(TestCase):
|
||||
"""
|
||||
Tests for L{twisted.internet.protocol.ClientCreator}.
|
||||
"""
|
||||
|
||||
def _basicConnectTest(self, check):
|
||||
"""
|
||||
Helper for implementing a test to verify that one of the I{connect}
|
||||
methods of L{ClientCreator} passes the right arguments to the right
|
||||
reactor method.
|
||||
|
||||
@param check: A function which will be invoked with a reactor and a
|
||||
L{ClientCreator} instance and which should call one of the
|
||||
L{ClientCreator}'s I{connect} methods and assert that all of its
|
||||
arguments except for the factory are passed on as expected to the
|
||||
reactor. The factory should be returned.
|
||||
"""
|
||||
|
||||
class SomeProtocol(Protocol):
|
||||
pass
|
||||
|
||||
reactor = MemoryReactorClock()
|
||||
cc = ClientCreator(reactor, SomeProtocol)
|
||||
factory = check(reactor, cc)
|
||||
protocol = factory.buildProtocol(None)
|
||||
self.assertIsInstance(protocol, SomeProtocol)
|
||||
|
||||
def test_connectTCP(self):
|
||||
"""
|
||||
L{ClientCreator.connectTCP} calls C{reactor.connectTCP} with the host
|
||||
and port information passed to it, and with a factory which will
|
||||
construct the protocol passed to L{ClientCreator.__init__}.
|
||||
"""
|
||||
|
||||
def check(reactor, cc):
|
||||
cc.connectTCP("example.com", 1234, 4321, ("1.2.3.4", 9876))
|
||||
host, port, factory, timeout, bindAddress = reactor.tcpClients.pop()
|
||||
self.assertEqual(host, "example.com")
|
||||
self.assertEqual(port, 1234)
|
||||
self.assertEqual(timeout, 4321)
|
||||
self.assertEqual(bindAddress, ("1.2.3.4", 9876))
|
||||
return factory
|
||||
|
||||
self._basicConnectTest(check)
|
||||
|
||||
def test_connectUNIX(self):
|
||||
"""
|
||||
L{ClientCreator.connectUNIX} calls C{reactor.connectUNIX} with the
|
||||
filename passed to it, and with a factory which will construct the
|
||||
protocol passed to L{ClientCreator.__init__}.
|
||||
"""
|
||||
|
||||
def check(reactor, cc):
|
||||
cc.connectUNIX("/foo/bar", 123, True)
|
||||
address, factory, timeout, checkPID = reactor.unixClients.pop()
|
||||
self.assertEqual(address, "/foo/bar")
|
||||
self.assertEqual(timeout, 123)
|
||||
self.assertTrue(checkPID)
|
||||
return factory
|
||||
|
||||
self._basicConnectTest(check)
|
||||
|
||||
def test_connectSSL(self):
|
||||
"""
|
||||
L{ClientCreator.connectSSL} calls C{reactor.connectSSL} with the host,
|
||||
port, and context factory passed to it, and with a factory which will
|
||||
construct the protocol passed to L{ClientCreator.__init__}.
|
||||
"""
|
||||
|
||||
def check(reactor, cc):
|
||||
expectedContextFactory = object()
|
||||
cc.connectSSL(
|
||||
"example.com", 1234, expectedContextFactory, 4321, ("4.3.2.1", 5678)
|
||||
)
|
||||
(
|
||||
host,
|
||||
port,
|
||||
factory,
|
||||
contextFactory,
|
||||
timeout,
|
||||
bindAddress,
|
||||
) = reactor.sslClients.pop()
|
||||
self.assertEqual(host, "example.com")
|
||||
self.assertEqual(port, 1234)
|
||||
self.assertIs(contextFactory, expectedContextFactory)
|
||||
self.assertEqual(timeout, 4321)
|
||||
self.assertEqual(bindAddress, ("4.3.2.1", 5678))
|
||||
return factory
|
||||
|
||||
self._basicConnectTest(check)
|
||||
|
||||
def _cancelConnectTest(self, connect):
|
||||
"""
|
||||
Helper for implementing a test to verify that cancellation of the
|
||||
L{Deferred} returned by one of L{ClientCreator}'s I{connect} methods is
|
||||
implemented to cancel the underlying connector.
|
||||
|
||||
@param connect: A function which will be invoked with a L{ClientCreator}
|
||||
instance as an argument and which should call one its I{connect}
|
||||
methods and return the result.
|
||||
|
||||
@return: A L{Deferred} which fires when the test is complete or fails if
|
||||
there is a problem.
|
||||
"""
|
||||
reactor = MemoryReactorClock()
|
||||
cc = ClientCreator(reactor, Protocol)
|
||||
d = connect(cc)
|
||||
connector = reactor.connectors.pop()
|
||||
self.assertFalse(connector._disconnected)
|
||||
d.cancel()
|
||||
self.assertTrue(connector._disconnected)
|
||||
return self.assertFailure(d, CancelledError)
|
||||
|
||||
def test_cancelConnectTCP(self):
|
||||
"""
|
||||
The L{Deferred} returned by L{ClientCreator.connectTCP} can be cancelled
|
||||
to abort the connection attempt before it completes.
|
||||
"""
|
||||
|
||||
def connect(cc):
|
||||
return cc.connectTCP("example.com", 1234)
|
||||
|
||||
return self._cancelConnectTest(connect)
|
||||
|
||||
def test_cancelConnectUNIX(self):
|
||||
"""
|
||||
The L{Deferred} returned by L{ClientCreator.connectTCP} can be cancelled
|
||||
to abort the connection attempt before it completes.
|
||||
"""
|
||||
|
||||
def connect(cc):
|
||||
return cc.connectUNIX("/foo/bar")
|
||||
|
||||
return self._cancelConnectTest(connect)
|
||||
|
||||
def test_cancelConnectSSL(self):
|
||||
"""
|
||||
The L{Deferred} returned by L{ClientCreator.connectTCP} can be cancelled
|
||||
to abort the connection attempt before it completes.
|
||||
"""
|
||||
|
||||
def connect(cc):
|
||||
return cc.connectSSL("example.com", 1234, object())
|
||||
|
||||
return self._cancelConnectTest(connect)
|
||||
|
||||
def _cancelConnectTimeoutTest(self, connect):
|
||||
"""
|
||||
Like L{_cancelConnectTest}, but for the case where the L{Deferred} is
|
||||
cancelled after the connection is set up but before it is fired with the
|
||||
resulting protocol instance.
|
||||
"""
|
||||
reactor = MemoryReactorClock()
|
||||
cc = ClientCreator(reactor, Protocol)
|
||||
d = connect(reactor, cc)
|
||||
connector = reactor.connectors.pop()
|
||||
# Sanity check - there is an outstanding delayed call to fire the
|
||||
# Deferred.
|
||||
self.assertEqual(len(reactor.getDelayedCalls()), 1)
|
||||
|
||||
# Cancel the Deferred, disconnecting the transport just set up and
|
||||
# cancelling the delayed call.
|
||||
d.cancel()
|
||||
|
||||
self.assertEqual(reactor.getDelayedCalls(), [])
|
||||
|
||||
# A real connector implementation is responsible for disconnecting the
|
||||
# transport as well. For our purposes, just check that someone told the
|
||||
# connector to disconnect.
|
||||
self.assertTrue(connector._disconnected)
|
||||
|
||||
return self.assertFailure(d, CancelledError)
|
||||
|
||||
def test_cancelConnectTCPTimeout(self):
|
||||
"""
|
||||
L{ClientCreator.connectTCP} inserts a very short delayed call between
|
||||
the time the connection is established and the time the L{Deferred}
|
||||
returned from one of its connect methods actually fires. If the
|
||||
L{Deferred} is cancelled in this interval, the established connection is
|
||||
closed, the timeout is cancelled, and the L{Deferred} fails with
|
||||
L{CancelledError}.
|
||||
"""
|
||||
|
||||
def connect(reactor, cc):
|
||||
d = cc.connectTCP("example.com", 1234)
|
||||
host, port, factory, timeout, bindAddress = reactor.tcpClients.pop()
|
||||
protocol = factory.buildProtocol(None)
|
||||
transport = StringTransport()
|
||||
protocol.makeConnection(transport)
|
||||
return d
|
||||
|
||||
return self._cancelConnectTimeoutTest(connect)
|
||||
|
||||
def test_cancelConnectUNIXTimeout(self):
|
||||
"""
|
||||
L{ClientCreator.connectUNIX} inserts a very short delayed call between
|
||||
the time the connection is established and the time the L{Deferred}
|
||||
returned from one of its connect methods actually fires. If the
|
||||
L{Deferred} is cancelled in this interval, the established connection is
|
||||
closed, the timeout is cancelled, and the L{Deferred} fails with
|
||||
L{CancelledError}.
|
||||
"""
|
||||
|
||||
def connect(reactor, cc):
|
||||
d = cc.connectUNIX("/foo/bar")
|
||||
address, factory, timeout, bindAddress = reactor.unixClients.pop()
|
||||
protocol = factory.buildProtocol(None)
|
||||
transport = StringTransport()
|
||||
protocol.makeConnection(transport)
|
||||
return d
|
||||
|
||||
return self._cancelConnectTimeoutTest(connect)
|
||||
|
||||
def test_cancelConnectSSLTimeout(self):
|
||||
"""
|
||||
L{ClientCreator.connectSSL} inserts a very short delayed call between
|
||||
the time the connection is established and the time the L{Deferred}
|
||||
returned from one of its connect methods actually fires. If the
|
||||
L{Deferred} is cancelled in this interval, the established connection is
|
||||
closed, the timeout is cancelled, and the L{Deferred} fails with
|
||||
L{CancelledError}.
|
||||
"""
|
||||
|
||||
def connect(reactor, cc):
|
||||
d = cc.connectSSL("example.com", 1234, object())
|
||||
(
|
||||
host,
|
||||
port,
|
||||
factory,
|
||||
contextFactory,
|
||||
timeout,
|
||||
bindADdress,
|
||||
) = reactor.sslClients.pop()
|
||||
protocol = factory.buildProtocol(None)
|
||||
transport = StringTransport()
|
||||
protocol.makeConnection(transport)
|
||||
return d
|
||||
|
||||
return self._cancelConnectTimeoutTest(connect)
|
||||
|
||||
def _cancelConnectFailedTimeoutTest(self, connect):
|
||||
"""
|
||||
Like L{_cancelConnectTest}, but for the case where the L{Deferred} is
|
||||
cancelled after the connection attempt has failed but before it is fired
|
||||
with the resulting failure.
|
||||
"""
|
||||
reactor = MemoryReactorClock()
|
||||
cc = ClientCreator(reactor, Protocol)
|
||||
d, factory = connect(reactor, cc)
|
||||
connector = reactor.connectors.pop()
|
||||
factory.clientConnectionFailed(
|
||||
connector, Failure(Exception("Simulated failure"))
|
||||
)
|
||||
|
||||
# Sanity check - there is an outstanding delayed call to fire the
|
||||
# Deferred.
|
||||
self.assertEqual(len(reactor.getDelayedCalls()), 1)
|
||||
|
||||
# Cancel the Deferred, cancelling the delayed call.
|
||||
d.cancel()
|
||||
|
||||
self.assertEqual(reactor.getDelayedCalls(), [])
|
||||
|
||||
return self.assertFailure(d, CancelledError)
|
||||
|
||||
def test_cancelConnectTCPFailedTimeout(self):
|
||||
"""
|
||||
Similar to L{test_cancelConnectTCPTimeout}, but for the case where the
|
||||
connection attempt fails.
|
||||
"""
|
||||
|
||||
def connect(reactor, cc):
|
||||
d = cc.connectTCP("example.com", 1234)
|
||||
host, port, factory, timeout, bindAddress = reactor.tcpClients.pop()
|
||||
return d, factory
|
||||
|
||||
return self._cancelConnectFailedTimeoutTest(connect)
|
||||
|
||||
def test_cancelConnectUNIXFailedTimeout(self):
|
||||
"""
|
||||
Similar to L{test_cancelConnectUNIXTimeout}, but for the case where the
|
||||
connection attempt fails.
|
||||
"""
|
||||
|
||||
def connect(reactor, cc):
|
||||
d = cc.connectUNIX("/foo/bar")
|
||||
address, factory, timeout, bindAddress = reactor.unixClients.pop()
|
||||
return d, factory
|
||||
|
||||
return self._cancelConnectFailedTimeoutTest(connect)
|
||||
|
||||
def test_cancelConnectSSLFailedTimeout(self):
|
||||
"""
|
||||
Similar to L{test_cancelConnectSSLTimeout}, but for the case where the
|
||||
connection attempt fails.
|
||||
"""
|
||||
|
||||
def connect(reactor, cc):
|
||||
d = cc.connectSSL("example.com", 1234, object())
|
||||
(
|
||||
host,
|
||||
port,
|
||||
factory,
|
||||
contextFactory,
|
||||
timeout,
|
||||
bindADdress,
|
||||
) = reactor.sslClients.pop()
|
||||
return d, factory
|
||||
|
||||
return self._cancelConnectFailedTimeoutTest(connect)
|
||||
|
||||
|
||||
class ProtocolTests(TestCase):
|
||||
"""
|
||||
Tests for L{twisted.internet.protocol.Protocol}.
|
||||
"""
|
||||
|
||||
def test_interfaces(self):
|
||||
"""
|
||||
L{Protocol} instances provide L{IProtocol} and L{ILoggingContext}.
|
||||
"""
|
||||
proto = Protocol()
|
||||
self.assertTrue(verifyObject(IProtocol, proto))
|
||||
self.assertTrue(verifyObject(ILoggingContext, proto))
|
||||
|
||||
def test_logPrefix(self):
|
||||
"""
|
||||
L{Protocol.logPrefix} returns the protocol class's name.
|
||||
"""
|
||||
|
||||
class SomeThing(Protocol):
|
||||
pass
|
||||
|
||||
self.assertEqual("SomeThing", SomeThing().logPrefix())
|
||||
|
||||
def test_makeConnection(self):
|
||||
"""
|
||||
L{Protocol.makeConnection} sets the given transport on itself, and
|
||||
then calls C{connectionMade}.
|
||||
"""
|
||||
result = []
|
||||
|
||||
class SomeProtocol(Protocol):
|
||||
def connectionMade(self):
|
||||
result.append(self.transport)
|
||||
|
||||
transport = object()
|
||||
protocol = SomeProtocol()
|
||||
protocol.makeConnection(transport)
|
||||
self.assertEqual(result, [transport])
|
||||
|
||||
|
||||
class FactoryTests(TestCase):
|
||||
"""
|
||||
Tests for L{protocol.Factory}.
|
||||
"""
|
||||
|
||||
def test_interfaces(self):
|
||||
"""
|
||||
L{Factory} instances provide both L{IProtocolFactory} and
|
||||
L{ILoggingContext}.
|
||||
"""
|
||||
factory = Factory()
|
||||
self.assertTrue(verifyObject(IProtocolFactory, factory))
|
||||
self.assertTrue(verifyObject(ILoggingContext, factory))
|
||||
|
||||
def test_logPrefix(self):
|
||||
"""
|
||||
L{Factory.logPrefix} returns the name of the factory class.
|
||||
"""
|
||||
|
||||
class SomeKindOfFactory(Factory):
|
||||
pass
|
||||
|
||||
self.assertEqual("SomeKindOfFactory", SomeKindOfFactory().logPrefix())
|
||||
|
||||
def test_defaultBuildProtocol(self):
|
||||
"""
|
||||
L{Factory.buildProtocol} by default constructs a protocol by calling
|
||||
its C{protocol} attribute, and attaches the factory to the result.
|
||||
"""
|
||||
|
||||
class SomeProtocol(Protocol):
|
||||
pass
|
||||
|
||||
f = Factory()
|
||||
f.protocol = SomeProtocol
|
||||
protocol = f.buildProtocol(None)
|
||||
self.assertIsInstance(protocol, SomeProtocol)
|
||||
self.assertIs(protocol.factory, f)
|
||||
|
||||
def test_forProtocol(self):
|
||||
"""
|
||||
L{Factory.forProtocol} constructs a Factory, passing along any
|
||||
additional arguments, and sets its C{protocol} attribute to the given
|
||||
Protocol subclass.
|
||||
"""
|
||||
|
||||
class ArgTakingFactory(Factory):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.args, self.kwargs = args, kwargs
|
||||
|
||||
factory = ArgTakingFactory.forProtocol(Protocol, 1, 2, foo=12)
|
||||
self.assertEqual(factory.protocol, Protocol)
|
||||
self.assertEqual(factory.args, (1, 2))
|
||||
self.assertEqual(factory.kwargs, {"foo": 12})
|
||||
|
||||
def test_doStartLoggingStatement(self):
|
||||
"""
|
||||
L{Factory.doStart} logs that it is starting a factory, followed by
|
||||
the L{repr} of the L{Factory} instance that is being started.
|
||||
"""
|
||||
events = []
|
||||
globalLogPublisher.addObserver(events.append)
|
||||
self.addCleanup(lambda: globalLogPublisher.removeObserver(events.append))
|
||||
|
||||
f = Factory()
|
||||
f.doStart()
|
||||
|
||||
self.assertIs(events[0]["factory"], f)
|
||||
self.assertEqual(events[0]["log_level"], LogLevel.info)
|
||||
self.assertEqual(events[0]["log_format"], "Starting factory {factory!r}")
|
||||
|
||||
def test_doStopLoggingStatement(self):
|
||||
"""
|
||||
L{Factory.doStop} logs that it is stopping a factory, followed by
|
||||
the L{repr} of the L{Factory} instance that is being stopped.
|
||||
"""
|
||||
events = []
|
||||
globalLogPublisher.addObserver(events.append)
|
||||
self.addCleanup(lambda: globalLogPublisher.removeObserver(events.append))
|
||||
|
||||
class MyFactory(Factory):
|
||||
numPorts = 1
|
||||
|
||||
f = MyFactory()
|
||||
f.doStop()
|
||||
|
||||
self.assertIs(events[0]["factory"], f)
|
||||
self.assertEqual(events[0]["log_level"], LogLevel.info)
|
||||
self.assertEqual(events[0]["log_format"], "Stopping factory {factory!r}")
|
||||
|
||||
|
||||
class AdapterTests(TestCase):
|
||||
"""
|
||||
Tests for L{ProtocolToConsumerAdapter} and L{ConsumerToProtocolAdapter}.
|
||||
"""
|
||||
|
||||
def test_protocolToConsumer(self):
|
||||
"""
|
||||
L{IProtocol} providers can be adapted to L{IConsumer} providers using
|
||||
L{ProtocolToConsumerAdapter}.
|
||||
"""
|
||||
result = []
|
||||
p = Protocol()
|
||||
p.dataReceived = result.append
|
||||
consumer = IConsumer(p)
|
||||
consumer.write(b"hello")
|
||||
self.assertEqual(result, [b"hello"])
|
||||
self.assertIsInstance(consumer, ProtocolToConsumerAdapter)
|
||||
|
||||
def test_consumerToProtocol(self):
|
||||
"""
|
||||
L{IConsumer} providers can be adapted to L{IProtocol} providers using
|
||||
L{ProtocolToConsumerAdapter}.
|
||||
"""
|
||||
result = []
|
||||
|
||||
@implementer(IConsumer)
|
||||
class Consumer:
|
||||
def write(self, d):
|
||||
result.append(d)
|
||||
|
||||
c = Consumer()
|
||||
protocol = IProtocol(c)
|
||||
protocol.dataReceived(b"hello")
|
||||
self.assertEqual(result, [b"hello"])
|
||||
self.assertIsInstance(protocol, ConsumerToProtocolAdapter)
|
||||
|
||||
|
||||
class FileWrapperTests(TestCase):
|
||||
"""
|
||||
L{twisted.internet.protocol.FileWrapper}
|
||||
"""
|
||||
|
||||
def test_write(self):
|
||||
"""
|
||||
L{twisted.internet.protocol.FileWrapper.write}
|
||||
"""
|
||||
wrapper = FileWrapper(BytesIO())
|
||||
wrapper.write(b"test1")
|
||||
self.assertEqual(wrapper.file.getvalue(), b"test1")
|
||||
|
||||
wrapper = FileWrapper(BytesIO())
|
||||
# BytesIO() cannot accept unicode, so this will
|
||||
# cause an exception to be thrown which will be
|
||||
# handled by FileWrapper.handle_exception().
|
||||
wrapper.write("stuff")
|
||||
self.assertNotEqual(wrapper.file.getvalue(), "stuff")
|
||||
|
||||
def test_writeSequence(self):
|
||||
"""
|
||||
L{twisted.internet.protocol.FileWrapper.writeSequence}
|
||||
"""
|
||||
wrapper = FileWrapper(BytesIO())
|
||||
wrapper.writeSequence([b"test1", b"test2"])
|
||||
self.assertEqual(wrapper.file.getvalue(), b"test1test2")
|
||||
|
||||
wrapper = FileWrapper(BytesIO())
|
||||
# In Python 3, b"".join([u"a", u"b"]) will raise a TypeError
|
||||
self.assertRaises(TypeError, wrapper.writeSequence, ["test3", "test4"])
|
||||
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Tests L{twisted.internet.test.reactormixins}, the reactor-testing support
|
||||
module.
|
||||
"""
|
||||
|
||||
from hamcrest import assert_that, equal_to, has_length
|
||||
from typing_extensions import NoReturn
|
||||
|
||||
# Trial should expose matches_result publically.
|
||||
# https://github.com/twisted/twisted/issues/11709
|
||||
from twisted.trial._dist.test.matchers import matches_result
|
||||
from twisted.trial.reporter import TestResult
|
||||
from twisted.trial.runner import TestLoader
|
||||
from twisted.trial.unittest import SynchronousTestCase, TestSuite
|
||||
from .reactormixins import ReactorBuilder
|
||||
|
||||
UNSUPPORTED = "This reactor is unsupported."
|
||||
|
||||
|
||||
def unsupportedReactor(self: ReactorBuilder) -> NoReturn:
|
||||
"""
|
||||
A function that can be used as a factory for L{ReactorBuilder} tests but
|
||||
which always raises an exception.
|
||||
|
||||
This gives the appearance of a reactor type which is unsupported in the
|
||||
current runtime configuration for some reason.
|
||||
"""
|
||||
raise Exception(UNSUPPORTED)
|
||||
|
||||
|
||||
class ReactorBuilderTests(SynchronousTestCase):
|
||||
"""
|
||||
Tests for L{ReactorBuilder}.
|
||||
"""
|
||||
|
||||
def test_buildReactorFails(self) -> None:
|
||||
"""
|
||||
If the reactor factory raises any exception then
|
||||
L{ReactorBuilder.buildReactor} raises L{SkipTest}.
|
||||
"""
|
||||
|
||||
class BrokenReactorFactory(ReactorBuilder, SynchronousTestCase):
|
||||
_reactors = [
|
||||
"twisted.internet.test.test_reactormixins.unsupportedReactor",
|
||||
]
|
||||
|
||||
def test_brokenFactory(self) -> None:
|
||||
"""
|
||||
Try, and fail, to build an unsupported reactor.
|
||||
"""
|
||||
self.buildReactor()
|
||||
|
||||
cases = BrokenReactorFactory.makeTestCaseClasses().values()
|
||||
loader = TestLoader()
|
||||
suite = TestSuite(loader.loadClass(cls) for cls in cases)
|
||||
result = TestResult()
|
||||
suite.run(result)
|
||||
|
||||
assert_that(result, matches_result(skips=has_length(1)))
|
||||
[(_, skip)] = result.skips
|
||||
assert_that(skip, equal_to(UNSUPPORTED))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user