241 lines
6.7 KiB
Python
241 lines
6.7 KiB
Python
import os
|
|
import secrets
|
|
import sys
|
|
from contextlib import suppress
|
|
from subprocess import PIPE, Popen, TimeoutExpired
|
|
from tempfile import TemporaryDirectory
|
|
from textwrap import dedent
|
|
from threading import Timer
|
|
from time import sleep
|
|
|
|
import pytest
|
|
|
|
# We need to interrupt the autoreloader without killing it, so that the server gets terminated
|
|
# https://stefan.sofa-rockers.org/2013/08/15/handling-sub-process-hierarchies-python-linux-os-x/
|
|
|
|
try:
|
|
from signal import CTRL_BREAK_EVENT
|
|
from subprocess import CREATE_NEW_PROCESS_GROUP
|
|
|
|
flags = CREATE_NEW_PROCESS_GROUP
|
|
except ImportError:
|
|
flags = 0
|
|
|
|
TIMER_DELAY = 2
|
|
|
|
|
|
def terminate(proc):
|
|
if flags:
|
|
proc.send_signal(CTRL_BREAK_EVENT)
|
|
else:
|
|
proc.terminate()
|
|
|
|
|
|
def write_app(filename, **runargs):
|
|
text = secrets.token_urlsafe()
|
|
with open(filename, "w") as f:
|
|
f.write(
|
|
dedent(
|
|
f"""\
|
|
import os
|
|
from sanic import Sanic
|
|
|
|
app = Sanic(__name__)
|
|
|
|
app.route("/")(lambda x: x)
|
|
|
|
@app.listener("after_server_start")
|
|
def complete(*args):
|
|
print("complete", os.getpid(), {text!r})
|
|
|
|
if __name__ == "__main__":
|
|
app.run(**{runargs!r})
|
|
"""
|
|
)
|
|
)
|
|
return text
|
|
|
|
|
|
def write_listener_app(filename, **runargs):
|
|
start_text = secrets.token_urlsafe()
|
|
stop_text = secrets.token_urlsafe()
|
|
with open(filename, "w") as f:
|
|
f.write(
|
|
dedent(
|
|
f"""\
|
|
import os
|
|
from sanic import Sanic
|
|
|
|
app = Sanic(__name__)
|
|
|
|
app.route("/")(lambda x: x)
|
|
|
|
@app.reload_process_start
|
|
async def reload_start(*_):
|
|
print("reload_start", os.getpid(), {start_text!r})
|
|
|
|
@app.reload_process_stop
|
|
async def reload_stop(*_):
|
|
print("reload_stop", os.getpid(), {stop_text!r})
|
|
|
|
if __name__ == "__main__":
|
|
app.run(**{runargs!r})
|
|
"""
|
|
)
|
|
)
|
|
return start_text, stop_text
|
|
|
|
|
|
def write_json_config_app(filename, jsonfile, **runargs):
|
|
with open(filename, "w") as f:
|
|
f.write(
|
|
dedent(
|
|
f"""\
|
|
import os
|
|
from sanic import Sanic
|
|
import json
|
|
|
|
app = Sanic(__name__)
|
|
with open("{jsonfile}", "r") as f:
|
|
config = json.load(f)
|
|
app.config.update_config(config)
|
|
|
|
app.route("/")(lambda x: x)
|
|
|
|
@app.listener("after_server_start")
|
|
def complete(*args):
|
|
print("complete", os.getpid(), app.config.FOO)
|
|
|
|
if __name__ == "__main__":
|
|
app.run(**{runargs!r})
|
|
"""
|
|
)
|
|
)
|
|
|
|
|
|
def write_file(filename):
|
|
text = secrets.token_urlsafe()
|
|
with open(filename, "w") as f:
|
|
f.write(f"""{{"FOO": "{text}"}}""")
|
|
return text
|
|
|
|
|
|
def scanner(proc, trigger="complete"):
|
|
for line in proc.stdout:
|
|
line = line.decode().strip()
|
|
if line.startswith(trigger):
|
|
yield line
|
|
|
|
|
|
argv = {
|
|
"script": [sys.executable, "reloader.py"],
|
|
"module": [sys.executable, "-m", "reloader"],
|
|
"sanic": [
|
|
sys.executable,
|
|
"-m",
|
|
"sanic",
|
|
"--port",
|
|
"42204",
|
|
"--auto-reload",
|
|
"reloader.app",
|
|
],
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"runargs, mode",
|
|
[
|
|
({"port": 42202, "auto_reload": True}, "script"),
|
|
({"port": 42203, "auto_reload": True}, "module"),
|
|
({}, "sanic"),
|
|
],
|
|
)
|
|
@pytest.mark.xfail
|
|
async def test_reloader_live(runargs, mode):
|
|
with TemporaryDirectory() as tmpdir:
|
|
filename = os.path.join(tmpdir, "reloader.py")
|
|
text = write_app(filename, **runargs)
|
|
command = argv[mode]
|
|
proc = Popen(command, cwd=tmpdir, stdout=PIPE, creationflags=flags)
|
|
try:
|
|
timeout = Timer(TIMER_DELAY, terminate, [proc])
|
|
timeout.start()
|
|
# Python apparently keeps using the old source sometimes if
|
|
# we don't sleep before rewrite (pycache timestamp problem?)
|
|
sleep(1)
|
|
line = scanner(proc)
|
|
assert text in next(line)
|
|
# Edit source code and try again
|
|
text = write_app(filename, **runargs)
|
|
assert text in next(line)
|
|
finally:
|
|
timeout.cancel()
|
|
terminate(proc)
|
|
with suppress(TimeoutExpired):
|
|
proc.wait(timeout=3)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"runargs, mode",
|
|
[
|
|
({"port": 42302, "auto_reload": True}, "script"),
|
|
({"port": 42303, "auto_reload": True}, "module"),
|
|
({}, "sanic"),
|
|
],
|
|
)
|
|
@pytest.mark.xfail
|
|
async def test_reloader_live_with_dir(runargs, mode):
|
|
with TemporaryDirectory() as tmpdir:
|
|
filename = os.path.join(tmpdir, "reloader.py")
|
|
config_file = os.path.join(tmpdir, "config.json")
|
|
runargs["reload_dir"] = tmpdir
|
|
write_json_config_app(filename, config_file, **runargs)
|
|
text = write_file(config_file)
|
|
command = argv[mode]
|
|
if mode == "sanic":
|
|
command += ["--reload-dir", tmpdir]
|
|
proc = Popen(command, cwd=tmpdir, stdout=PIPE, creationflags=flags)
|
|
try:
|
|
timeout = Timer(TIMER_DELAY, terminate, [proc])
|
|
timeout.start()
|
|
# Python apparently keeps using the old source sometimes if
|
|
# we don't sleep before rewrite (pycache timestamp problem?)
|
|
sleep(1)
|
|
line = scanner(proc)
|
|
assert text in next(line)
|
|
# Edit source code and try again
|
|
text = write_file(config_file)
|
|
assert text in next(line)
|
|
finally:
|
|
timeout.cancel()
|
|
terminate(proc)
|
|
with suppress(TimeoutExpired):
|
|
proc.wait(timeout=3)
|
|
|
|
|
|
def test_reload_listeners():
|
|
with TemporaryDirectory() as tmpdir:
|
|
filename = os.path.join(tmpdir, "reloader.py")
|
|
start_text, stop_text = write_listener_app(
|
|
filename, port=42305, auto_reload=True
|
|
)
|
|
|
|
proc = Popen(
|
|
argv["script"], cwd=tmpdir, stdout=PIPE, creationflags=flags
|
|
)
|
|
try:
|
|
timeout = Timer(TIMER_DELAY, terminate, [proc])
|
|
timeout.start()
|
|
# Python apparently keeps using the old source sometimes if
|
|
# we don't sleep before rewrite (pycache timestamp problem?)
|
|
sleep(1)
|
|
line = scanner(proc, "reload_start")
|
|
assert start_text in next(line)
|
|
line = scanner(proc, "reload_stop")
|
|
assert stop_text in next(line)
|
|
finally:
|
|
timeout.cancel()
|
|
terminate(proc)
|
|
with suppress(TimeoutExpired):
|
|
proc.wait(timeout=3)
|