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