twisted_test.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. # Author: Ovidiu Predescu
  2. # Date: July 2011
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. # not use this file except in compliance with the License. You may obtain
  6. # a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. # License for the specific language governing permissions and limitations
  14. # under the License.
  15. import asyncio
  16. import logging
  17. import signal
  18. import unittest
  19. import warnings
  20. from tornado.escape import utf8
  21. from tornado import gen
  22. from tornado.httpclient import AsyncHTTPClient
  23. from tornado.httpserver import HTTPServer
  24. from tornado.ioloop import IOLoop
  25. from tornado.testing import bind_unused_port, AsyncTestCase, gen_test
  26. from tornado.web import RequestHandler, Application
  27. try:
  28. from twisted.internet.defer import ( # type: ignore
  29. Deferred,
  30. inlineCallbacks,
  31. returnValue,
  32. )
  33. from twisted.internet.protocol import Protocol # type: ignore
  34. from twisted.internet.asyncioreactor import AsyncioSelectorReactor # type: ignore
  35. from twisted.web.client import Agent, readBody # type: ignore
  36. from twisted.web.resource import Resource # type: ignore
  37. from twisted.web.server import Site # type: ignore
  38. have_twisted = True
  39. except ImportError:
  40. have_twisted = False
  41. else:
  42. # Not used directly but needed for `yield deferred` to work.
  43. import tornado.platform.twisted # noqa: F401
  44. skipIfNoTwisted = unittest.skipUnless(have_twisted, "twisted module not present")
  45. def save_signal_handlers():
  46. saved = {}
  47. for sig in [signal.SIGINT, signal.SIGTERM, signal.SIGCHLD]:
  48. saved[sig] = signal.getsignal(sig)
  49. if "twisted" in repr(saved):
  50. # This indicates we're not cleaning up after ourselves properly.
  51. raise Exception("twisted signal handlers already installed")
  52. return saved
  53. def restore_signal_handlers(saved):
  54. for sig, handler in saved.items():
  55. signal.signal(sig, handler)
  56. # Test various combinations of twisted and tornado http servers,
  57. # http clients, and event loop interfaces.
  58. @skipIfNoTwisted
  59. class CompatibilityTests(unittest.TestCase):
  60. def setUp(self):
  61. self.saved_signals = save_signal_handlers()
  62. self.io_loop = IOLoop()
  63. self.io_loop.make_current()
  64. self.reactor = AsyncioSelectorReactor()
  65. def tearDown(self):
  66. self.reactor.disconnectAll()
  67. self.io_loop.clear_current()
  68. self.io_loop.close(all_fds=True)
  69. restore_signal_handlers(self.saved_signals)
  70. def start_twisted_server(self):
  71. class HelloResource(Resource):
  72. isLeaf = True
  73. def render_GET(self, request):
  74. return b"Hello from twisted!"
  75. site = Site(HelloResource())
  76. port = self.reactor.listenTCP(0, site, interface="127.0.0.1")
  77. self.twisted_port = port.getHost().port
  78. def start_tornado_server(self):
  79. class HelloHandler(RequestHandler):
  80. def get(self):
  81. self.write("Hello from tornado!")
  82. app = Application([("/", HelloHandler)], log_function=lambda x: None)
  83. server = HTTPServer(app)
  84. sock, self.tornado_port = bind_unused_port()
  85. server.add_sockets([sock])
  86. def run_reactor(self):
  87. # In theory, we can run the event loop through Tornado,
  88. # Twisted, or asyncio interfaces. However, since we're trying
  89. # to avoid installing anything as the global event loop, only
  90. # the twisted interface gets everything wired up correectly
  91. # without extra hacks. This method is a part of a
  92. # no-longer-used generalization that allowed us to test
  93. # different combinations.
  94. self.stop_loop = self.reactor.stop
  95. self.stop = self.reactor.stop
  96. self.reactor.run()
  97. def tornado_fetch(self, url, runner):
  98. client = AsyncHTTPClient()
  99. fut = asyncio.ensure_future(client.fetch(url))
  100. fut.add_done_callback(lambda f: self.stop_loop())
  101. runner()
  102. return fut.result()
  103. def twisted_fetch(self, url, runner):
  104. # http://twistedmatrix.com/documents/current/web/howto/client.html
  105. chunks = []
  106. client = Agent(self.reactor)
  107. d = client.request(b"GET", utf8(url))
  108. class Accumulator(Protocol):
  109. def __init__(self, finished):
  110. self.finished = finished
  111. def dataReceived(self, data):
  112. chunks.append(data)
  113. def connectionLost(self, reason):
  114. self.finished.callback(None)
  115. def callback(response):
  116. finished = Deferred()
  117. response.deliverBody(Accumulator(finished))
  118. return finished
  119. d.addCallback(callback)
  120. def shutdown(failure):
  121. if hasattr(self, "stop_loop"):
  122. self.stop_loop()
  123. elif failure is not None:
  124. # loop hasn't been initialized yet; try our best to
  125. # get an error message out. (the runner() interaction
  126. # should probably be refactored).
  127. try:
  128. failure.raiseException()
  129. except:
  130. logging.error("exception before starting loop", exc_info=True)
  131. d.addBoth(shutdown)
  132. runner()
  133. self.assertTrue(chunks)
  134. return b"".join(chunks)
  135. def twisted_coroutine_fetch(self, url, runner):
  136. body = [None]
  137. @gen.coroutine
  138. def f():
  139. # This is simpler than the non-coroutine version, but it cheats
  140. # by reading the body in one blob instead of streaming it with
  141. # a Protocol.
  142. client = Agent(self.reactor)
  143. response = yield client.request(b"GET", utf8(url))
  144. with warnings.catch_warnings():
  145. # readBody has a buggy DeprecationWarning in Twisted 15.0:
  146. # https://twistedmatrix.com/trac/changeset/43379
  147. warnings.simplefilter("ignore", category=DeprecationWarning)
  148. body[0] = yield readBody(response)
  149. self.stop_loop()
  150. self.io_loop.add_callback(f)
  151. runner()
  152. return body[0]
  153. def testTwistedServerTornadoClientReactor(self):
  154. self.start_twisted_server()
  155. response = self.tornado_fetch(
  156. "http://127.0.0.1:%d" % self.twisted_port, self.run_reactor
  157. )
  158. self.assertEqual(response.body, b"Hello from twisted!")
  159. def testTornadoServerTwistedClientReactor(self):
  160. self.start_tornado_server()
  161. response = self.twisted_fetch(
  162. "http://127.0.0.1:%d" % self.tornado_port, self.run_reactor
  163. )
  164. self.assertEqual(response, b"Hello from tornado!")
  165. def testTornadoServerTwistedCoroutineClientReactor(self):
  166. self.start_tornado_server()
  167. response = self.twisted_coroutine_fetch(
  168. "http://127.0.0.1:%d" % self.tornado_port, self.run_reactor
  169. )
  170. self.assertEqual(response, b"Hello from tornado!")
  171. @skipIfNoTwisted
  172. class ConvertDeferredTest(AsyncTestCase):
  173. @gen_test
  174. def test_success(self):
  175. @inlineCallbacks
  176. def fn():
  177. if False:
  178. # inlineCallbacks doesn't work with regular functions;
  179. # must have a yield even if it's unreachable.
  180. yield
  181. returnValue(42)
  182. res = yield fn()
  183. self.assertEqual(res, 42)
  184. @gen_test
  185. def test_failure(self):
  186. @inlineCallbacks
  187. def fn():
  188. if False:
  189. yield
  190. 1 / 0
  191. with self.assertRaises(ZeroDivisionError):
  192. yield fn()
  193. if __name__ == "__main__":
  194. unittest.main()