autoreload_test.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. import os
  2. import shutil
  3. import subprocess
  4. from subprocess import Popen
  5. import sys
  6. from tempfile import mkdtemp
  7. import textwrap
  8. import time
  9. import unittest
  10. class AutoreloadTest(unittest.TestCase):
  11. def setUp(self):
  12. # When these tests fail the output sometimes exceeds the default maxDiff.
  13. self.maxDiff = 1024
  14. self.path = mkdtemp()
  15. # Most test apps run themselves twice via autoreload. The first time it manually triggers
  16. # a reload (could also do this by touching a file but this is faster since filesystem
  17. # timestamps are not necessarily high resolution). The second time it exits directly
  18. # so that the autoreload wrapper (if it is used) doesn't catch it.
  19. #
  20. # The last line of each such test's "main" program should be
  21. # exec(open("run_twice_magic.py").read())
  22. self.write_files(
  23. {
  24. "run_twice_magic.py": """
  25. import os
  26. import sys
  27. import tornado.autoreload
  28. sys.stdout.flush()
  29. if "TESTAPP_STARTED" not in os.environ:
  30. os.environ["TESTAPP_STARTED"] = "1"
  31. tornado.autoreload._reload()
  32. else:
  33. os._exit(0)
  34. """
  35. }
  36. )
  37. def tearDown(self):
  38. try:
  39. shutil.rmtree(self.path)
  40. except OSError:
  41. # Windows disallows deleting files that are in use by
  42. # another process, and even though we've waited for our
  43. # child process below, it appears that its lock on these
  44. # files is not guaranteed to be released by this point.
  45. # Sleep and try again (once).
  46. time.sleep(1)
  47. shutil.rmtree(self.path)
  48. def write_files(self, tree, base_path=None):
  49. """Write a directory tree to self.path.
  50. tree is a dictionary mapping file names to contents, or
  51. sub-dictionaries representing subdirectories.
  52. """
  53. if base_path is None:
  54. base_path = self.path
  55. for name, contents in tree.items():
  56. if isinstance(contents, dict):
  57. os.mkdir(os.path.join(base_path, name))
  58. self.write_files(contents, os.path.join(base_path, name))
  59. else:
  60. with open(os.path.join(base_path, name), "w", encoding="utf-8") as f:
  61. f.write(textwrap.dedent(contents))
  62. def run_subprocess(self, args):
  63. # Make sure the tornado module under test is available to the test
  64. # application
  65. parts = [os.getcwd()]
  66. if "PYTHONPATH" in os.environ:
  67. parts += [
  68. os.path.join(os.getcwd(), part)
  69. for part in os.environ["PYTHONPATH"].split(os.pathsep)
  70. ]
  71. pythonpath = os.pathsep.join(parts)
  72. p = Popen(
  73. args,
  74. stdout=subprocess.PIPE,
  75. env=dict(os.environ, PYTHONPATH=pythonpath),
  76. cwd=self.path,
  77. universal_newlines=True,
  78. encoding="utf-8",
  79. )
  80. # This timeout needs to be fairly generous for pypy due to jit
  81. # warmup costs.
  82. for i in range(40):
  83. if p.poll() is not None:
  84. break
  85. time.sleep(0.1)
  86. else:
  87. p.kill()
  88. raise Exception("subprocess failed to terminate")
  89. out = p.communicate()[0]
  90. self.assertEqual(p.returncode, 0)
  91. return out
  92. def test_reload(self):
  93. main = """\
  94. import sys
  95. # In module mode, the path is set to the parent directory and we can import testapp.
  96. try:
  97. import testapp
  98. except ImportError:
  99. print("import testapp failed")
  100. else:
  101. print("import testapp succeeded")
  102. spec = getattr(sys.modules[__name__], '__spec__', None)
  103. print(f"Starting {__name__=}, __spec__.name={getattr(spec, 'name', None)}")
  104. exec(open("run_twice_magic.py", encoding="utf-8").read())
  105. """
  106. # Create temporary test application
  107. self.write_files(
  108. {
  109. "testapp": {
  110. "__init__.py": "",
  111. "__main__.py": main,
  112. },
  113. }
  114. )
  115. # The autoreload wrapper should support all the same modes as the python interpreter.
  116. # The wrapper itself should have no effect on this test so we try all modes with and
  117. # without it.
  118. for wrapper in [False, True]:
  119. with self.subTest(wrapper=wrapper):
  120. with self.subTest(mode="module"):
  121. if wrapper:
  122. base_args = [sys.executable, "-m", "tornado.autoreload"]
  123. else:
  124. base_args = [sys.executable]
  125. # In module mode, the path is set to the parent directory and we can import
  126. # testapp. Also, the __spec__.name is set to the fully qualified module name.
  127. out = self.run_subprocess(base_args + ["-m", "testapp"])
  128. self.assertEqual(
  129. out,
  130. (
  131. "import testapp succeeded\n"
  132. + "Starting __name__='__main__', __spec__.name=testapp.__main__\n"
  133. )
  134. * 2,
  135. )
  136. with self.subTest(mode="file"):
  137. out = self.run_subprocess(base_args + ["testapp/__main__.py"])
  138. # In file mode, we do not expect the path to be set so we can import testapp,
  139. # but when the wrapper is used the -m argument to the python interpreter
  140. # does this for us.
  141. expect_import = (
  142. "import testapp succeeded"
  143. if wrapper
  144. else "import testapp failed"
  145. )
  146. # In file mode there is no qualified module spec.
  147. self.assertEqual(
  148. out,
  149. f"{expect_import}\nStarting __name__='__main__', __spec__.name=None\n"
  150. * 2,
  151. )
  152. with self.subTest(mode="directory"):
  153. # Running as a directory finds __main__.py like a module. It does not manipulate
  154. # sys.path but it does set a spec with a name of exactly __main__.
  155. out = self.run_subprocess(base_args + ["testapp"])
  156. expect_import = (
  157. "import testapp succeeded"
  158. if wrapper
  159. else "import testapp failed"
  160. )
  161. self.assertEqual(
  162. out,
  163. f"{expect_import}\nStarting __name__='__main__', __spec__.name=__main__\n"
  164. * 2,
  165. )
  166. def test_reload_wrapper_preservation(self):
  167. # This test verifies that when `python -m tornado.autoreload`
  168. # is used on an application that also has an internal
  169. # autoreload, the reload wrapper is preserved on restart.
  170. main = """\
  171. import sys
  172. # This import will fail if path is not set up correctly
  173. import testapp
  174. if 'tornado.autoreload' not in sys.modules:
  175. raise Exception('started without autoreload wrapper')
  176. print('Starting')
  177. exec(open("run_twice_magic.py", encoding="utf-8").read())
  178. """
  179. self.write_files(
  180. {
  181. "testapp": {
  182. "__init__.py": "",
  183. "__main__.py": main,
  184. },
  185. }
  186. )
  187. out = self.run_subprocess(
  188. [sys.executable, "-m", "tornado.autoreload", "-m", "testapp"]
  189. )
  190. self.assertEqual(out, "Starting\n" * 2)
  191. def test_reload_wrapper_args(self):
  192. main = """\
  193. import os
  194. import sys
  195. print(os.path.basename(sys.argv[0]))
  196. print(f'argv={sys.argv[1:]}')
  197. exec(open("run_twice_magic.py", encoding="utf-8").read())
  198. """
  199. # Create temporary test application
  200. self.write_files({"main.py": main})
  201. # Make sure the tornado module under test is available to the test
  202. # application
  203. out = self.run_subprocess(
  204. [
  205. sys.executable,
  206. "-m",
  207. "tornado.autoreload",
  208. "main.py",
  209. "arg1",
  210. "--arg2",
  211. "-m",
  212. "arg3",
  213. ],
  214. )
  215. self.assertEqual(out, "main.py\nargv=['arg1', '--arg2', '-m', 'arg3']\n" * 2)
  216. def test_reload_wrapper_until_success(self):
  217. main = """\
  218. import os
  219. import sys
  220. if "TESTAPP_STARTED" in os.environ:
  221. print("exiting cleanly")
  222. sys.exit(0)
  223. else:
  224. print("reloading")
  225. exec(open("run_twice_magic.py", encoding="utf-8").read())
  226. """
  227. # Create temporary test application
  228. self.write_files({"main.py": main})
  229. out = self.run_subprocess(
  230. [sys.executable, "-m", "tornado.autoreload", "--until-success", "main.py"]
  231. )
  232. self.assertEqual(out, "reloading\nexiting cleanly\n")