reactormixins.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. Utilities for unit testing reactor implementations.
  5. The main feature of this module is L{ReactorBuilder}, a base class for use when
  6. writing interface/blackbox tests for reactor implementations. Test case classes
  7. for reactor features should subclass L{ReactorBuilder} instead of
  8. L{SynchronousTestCase}. All of the features of L{SynchronousTestCase} will be
  9. available. Additionally, the tests will automatically be applied to all
  10. available reactor implementations.
  11. """
  12. from __future__ import division, absolute_import
  13. __metaclass__ = type
  14. __all__ = ['TestTimeoutError', 'ReactorBuilder', 'needsRunningReactor']
  15. import os, signal, time
  16. from twisted.trial.unittest import SynchronousTestCase, SkipTest
  17. from twisted.trial.util import DEFAULT_TIMEOUT_DURATION, acquireAttribute
  18. from twisted.python.runtime import platform
  19. from twisted.python.reflect import namedAny
  20. from twisted.python.deprecate import _fullyQualifiedName as fullyQualifiedName
  21. from twisted.python import log
  22. from twisted.python.failure import Failure
  23. from twisted.python.compat import _PY3
  24. # Access private APIs.
  25. if platform.isWindows():
  26. process = None
  27. else:
  28. from twisted.internet import process
  29. class TestTimeoutError(Exception):
  30. """
  31. The reactor was still running after the timeout period elapsed in
  32. L{ReactorBuilder.runReactor}.
  33. """
  34. def needsRunningReactor(reactor, thunk):
  35. """
  36. Various functions within these tests need an already-running reactor at
  37. some point. They need to stop the reactor when the test has completed, and
  38. that means calling reactor.stop(). However, reactor.stop() raises an
  39. exception if the reactor isn't already running, so if the L{Deferred} that
  40. a particular API under test returns fires synchronously (as especially an
  41. endpoint's C{connect()} method may do, if the connect is to a local
  42. interface address) then the test won't be able to stop the reactor being
  43. tested and finish. So this calls C{thunk} only once C{reactor} is running.
  44. (This is just an alias for
  45. L{twisted.internet.interfaces.IReactorCore.callWhenRunning} on the given
  46. reactor parameter, in order to centrally reference the above paragraph and
  47. repeating it everywhere as a comment.)
  48. @param reactor: the L{twisted.internet.interfaces.IReactorCore} under test
  49. @param thunk: a 0-argument callable, which eventually finishes the test in
  50. question, probably in a L{Deferred} callback.
  51. """
  52. reactor.callWhenRunning(thunk)
  53. def stopOnError(case, reactor, publisher=None):
  54. """
  55. Stop the reactor as soon as any error is logged on the given publisher.
  56. This is beneficial for tests which will wait for a L{Deferred} to fire
  57. before completing (by passing or failing). Certain implementation bugs may
  58. prevent the L{Deferred} from firing with any result at all (consider a
  59. protocol's {dataReceived} method that raises an exception: this exception
  60. is logged but it won't ever cause a L{Deferred} to fire). In that case the
  61. test would have to complete by timing out which is a much less desirable
  62. outcome than completing as soon as the unexpected error is encountered.
  63. @param case: A L{SynchronousTestCase} to use to clean up the necessary log
  64. observer when the test is over.
  65. @param reactor: The reactor to stop.
  66. @param publisher: A L{LogPublisher} to watch for errors. If L{None}, the
  67. global log publisher will be watched.
  68. """
  69. if publisher is None:
  70. from twisted.python import log as publisher
  71. running = [None]
  72. def stopIfError(event):
  73. if running and event.get('isError'):
  74. running.pop()
  75. reactor.stop()
  76. publisher.addObserver(stopIfError)
  77. case.addCleanup(publisher.removeObserver, stopIfError)
  78. class ReactorBuilder:
  79. """
  80. L{SynchronousTestCase} mixin which provides a reactor-creation API. This
  81. mixin defines C{setUp} and C{tearDown}, so mix it in before
  82. L{SynchronousTestCase} or call its methods from the overridden ones in the
  83. subclass.
  84. @cvar skippedReactors: A dict mapping FQPN strings of reactors for
  85. which the tests defined by this class will be skipped to strings
  86. giving the skip message.
  87. @cvar requiredInterfaces: A C{list} of interfaces which the reactor must
  88. provide or these tests will be skipped. The default, L{None}, means
  89. that no interfaces are required.
  90. @ivar reactorFactory: A no-argument callable which returns the reactor to
  91. use for testing.
  92. @ivar originalHandler: The SIGCHLD handler which was installed when setUp
  93. ran and which will be re-installed when tearDown runs.
  94. @ivar _reactors: A list of FQPN strings giving the reactors for which
  95. L{SynchronousTestCase}s will be created.
  96. """
  97. _reactors = [
  98. # Select works everywhere
  99. "twisted.internet.selectreactor.SelectReactor",
  100. ]
  101. if platform.isWindows():
  102. # PortableGtkReactor is only really interesting on Windows,
  103. # but not really Windows specific; if you want you can
  104. # temporarily move this up to the all-platforms list to test
  105. # it on other platforms. It's not there in general because
  106. # it's not _really_ worth it to support on other platforms,
  107. # since no one really wants to use it on other platforms.
  108. _reactors.extend([
  109. "twisted.internet.gtk2reactor.PortableGtkReactor",
  110. "twisted.internet.gireactor.PortableGIReactor",
  111. "twisted.internet.gtk3reactor.PortableGtk3Reactor",
  112. "twisted.internet.win32eventreactor.Win32Reactor",
  113. "twisted.internet.iocpreactor.reactor.IOCPReactor"])
  114. else:
  115. _reactors.extend([
  116. "twisted.internet.glib2reactor.Glib2Reactor",
  117. "twisted.internet.gtk2reactor.Gtk2Reactor",
  118. "twisted.internet.gireactor.GIReactor",
  119. "twisted.internet.gtk3reactor.Gtk3Reactor"])
  120. if _PY3:
  121. _reactors.append(
  122. "twisted.internet.asyncioreactor.AsyncioSelectorReactor")
  123. if platform.isMacOSX():
  124. _reactors.append("twisted.internet.cfreactor.CFReactor")
  125. else:
  126. _reactors.extend([
  127. "twisted.internet.pollreactor.PollReactor",
  128. "twisted.internet.epollreactor.EPollReactor"])
  129. if not platform.isLinux():
  130. # Presumably Linux is not going to start supporting kqueue, so
  131. # skip even trying this configuration.
  132. _reactors.extend([
  133. # Support KQueue on non-OS-X POSIX platforms for now.
  134. "twisted.internet.kqreactor.KQueueReactor",
  135. ])
  136. reactorFactory = None
  137. originalHandler = None
  138. requiredInterfaces = None
  139. skippedReactors = {}
  140. def setUp(self):
  141. """
  142. Clear the SIGCHLD handler, if there is one, to ensure an environment
  143. like the one which exists prior to a call to L{reactor.run}.
  144. """
  145. if not platform.isWindows():
  146. self.originalHandler = signal.signal(signal.SIGCHLD, signal.SIG_DFL)
  147. def tearDown(self):
  148. """
  149. Restore the original SIGCHLD handler and reap processes as long as
  150. there seem to be any remaining.
  151. """
  152. if self.originalHandler is not None:
  153. signal.signal(signal.SIGCHLD, self.originalHandler)
  154. if process is not None:
  155. begin = time.time()
  156. while process.reapProcessHandlers:
  157. log.msg(
  158. "ReactorBuilder.tearDown reaping some processes %r" % (
  159. process.reapProcessHandlers,))
  160. process.reapAllProcesses()
  161. # The process should exit on its own. However, if it
  162. # doesn't, we're stuck in this loop forever. To avoid
  163. # hanging the test suite, eventually give the process some
  164. # help exiting and move on.
  165. time.sleep(0.001)
  166. if time.time() - begin > 60:
  167. for pid in process.reapProcessHandlers:
  168. os.kill(pid, signal.SIGKILL)
  169. raise Exception(
  170. "Timeout waiting for child processes to exit: %r" % (
  171. process.reapProcessHandlers,))
  172. def unbuildReactor(self, reactor):
  173. """
  174. Clean up any resources which may have been allocated for the given
  175. reactor by its creation or by a test which used it.
  176. """
  177. # Chris says:
  178. #
  179. # XXX These explicit calls to clean up the waker (and any other
  180. # internal readers) should become obsolete when bug #3063 is
  181. # fixed. -radix, 2008-02-29. Fortunately it should probably cause an
  182. # error when bug #3063 is fixed, so it should be removed in the same
  183. # branch that fixes it.
  184. #
  185. # -exarkun
  186. reactor._uninstallHandler()
  187. if getattr(reactor, '_internalReaders', None) is not None:
  188. for reader in reactor._internalReaders:
  189. reactor.removeReader(reader)
  190. reader.connectionLost(None)
  191. reactor._internalReaders.clear()
  192. # Here's an extra thing unrelated to wakers but necessary for
  193. # cleaning up after the reactors we make. -exarkun
  194. reactor.disconnectAll()
  195. # It would also be bad if any timed calls left over were allowed to
  196. # run.
  197. calls = reactor.getDelayedCalls()
  198. for c in calls:
  199. c.cancel()
  200. def buildReactor(self):
  201. """
  202. Create and return a reactor using C{self.reactorFactory}.
  203. """
  204. try:
  205. from twisted.internet.cfreactor import CFReactor
  206. from twisted.internet import reactor as globalReactor
  207. except ImportError:
  208. pass
  209. else:
  210. if (isinstance(globalReactor, CFReactor)
  211. and self.reactorFactory is CFReactor):
  212. raise SkipTest(
  213. "CFReactor uses APIs which manipulate global state, "
  214. "so it's not safe to run its own reactor-builder tests "
  215. "under itself")
  216. try:
  217. reactor = self.reactorFactory()
  218. except:
  219. # Unfortunately, not all errors which result in a reactor
  220. # being unusable are detectable without actually
  221. # instantiating the reactor. So we catch some more here
  222. # and skip the test if necessary. We also log it to aid
  223. # with debugging, but flush the logged error so the test
  224. # doesn't fail.
  225. log.err(None, "Failed to install reactor")
  226. self.flushLoggedErrors()
  227. raise SkipTest(Failure().getErrorMessage())
  228. else:
  229. if self.requiredInterfaces is not None:
  230. missing = [
  231. required for required in self.requiredInterfaces
  232. if not required.providedBy(reactor)]
  233. if missing:
  234. self.unbuildReactor(reactor)
  235. raise SkipTest("%s does not provide %s" % (
  236. fullyQualifiedName(reactor.__class__),
  237. ",".join([fullyQualifiedName(x) for x in missing])))
  238. self.addCleanup(self.unbuildReactor, reactor)
  239. return reactor
  240. def getTimeout(self):
  241. """
  242. Determine how long to run the test before considering it failed.
  243. @return: A C{int} or C{float} giving a number of seconds.
  244. """
  245. return acquireAttribute(self._parents, 'timeout', DEFAULT_TIMEOUT_DURATION)
  246. def runReactor(self, reactor, timeout=None):
  247. """
  248. Run the reactor for at most the given amount of time.
  249. @param reactor: The reactor to run.
  250. @type timeout: C{int} or C{float}
  251. @param timeout: The maximum amount of time, specified in seconds, to
  252. allow the reactor to run. If the reactor is still running after
  253. this much time has elapsed, it will be stopped and an exception
  254. raised. If L{None}, the default test method timeout imposed by
  255. Trial will be used. This depends on the L{IReactorTime}
  256. implementation of C{reactor} for correct operation.
  257. @raise TestTimeoutError: If the reactor is still running after
  258. C{timeout} seconds.
  259. """
  260. if timeout is None:
  261. timeout = self.getTimeout()
  262. timedOut = []
  263. def stop():
  264. timedOut.append(None)
  265. reactor.stop()
  266. timedOutCall = reactor.callLater(timeout, stop)
  267. reactor.run()
  268. if timedOut:
  269. raise TestTimeoutError(
  270. "reactor still running after %s seconds" % (timeout,))
  271. else:
  272. timedOutCall.cancel()
  273. def makeTestCaseClasses(cls):
  274. """
  275. Create a L{SynchronousTestCase} subclass which mixes in C{cls} for each
  276. known reactor and return a dict mapping their names to them.
  277. """
  278. classes = {}
  279. for reactor in cls._reactors:
  280. shortReactorName = reactor.split(".")[-1]
  281. name = (cls.__name__ + "." + shortReactorName + "Tests").replace(".", "_")
  282. class testcase(cls, SynchronousTestCase):
  283. __module__ = cls.__module__
  284. if reactor in cls.skippedReactors:
  285. skip = cls.skippedReactors[reactor]
  286. try:
  287. reactorFactory = namedAny(reactor)
  288. except:
  289. skip = Failure().getErrorMessage()
  290. testcase.__name__ = name
  291. if hasattr(cls, "__qualname__"):
  292. testcase.__qualname__ = ".".join(cls.__qualname__.split()[0:-1] + [name])
  293. classes[testcase.__name__] = testcase
  294. return classes
  295. makeTestCaseClasses = classmethod(makeTestCaseClasses)