100 lines
3.9 KiB
Python
100 lines
3.9 KiB
Python
"""Production Player reloads Lua atomically and keeps the old world on failure."""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
|
|
|
|
def behavior(marker, fail=False):
|
|
start = 'error("LUA_RELOAD_START_FAILED")' if fail else f'faset.log("LUA_START_{marker}")'
|
|
return f'''local behavior = faset.behavior {{
|
|
id = "test.lua.reload", version = 1, name = "Reload test", fields = {{}}
|
|
}}
|
|
function behavior:on_start() {start} end
|
|
function behavior:on_destroy() faset.log("LUA_DESTROY_{marker}") end
|
|
return behavior
|
|
'''
|
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix="faset-lua-reload-") as temporary:
|
|
root = Path(temporary)
|
|
(root / "Scripts").mkdir()
|
|
(root / "project.faset.json").write_text(json.dumps({
|
|
"format": "faset.project", "version": 1,
|
|
"scripting": {"lua": {"scripts": ["Scripts/reload.lua"]}}
|
|
}), encoding="utf-8")
|
|
source = root / "Scripts/reload.lua"
|
|
source.write_text(behavior("A"), encoding="utf-8")
|
|
scene = root / "scene.scene.json"
|
|
scene.write_text(json.dumps({
|
|
"format": "faset.scene", "version": 1, "id": "reload-scene", "dimension": 2,
|
|
"entities": [{"id": "object", "name": "Lua", "parent": None,
|
|
"components": [{"id": "lua", "type": "test.lua.reload",
|
|
"version": 1, "fields": {}}]}]
|
|
}), encoding="utf-8")
|
|
control = root / "control.json"
|
|
logs = []
|
|
process = subprocess.Popen([sys.argv[1], "--scene", str(scene), "--project", str(root),
|
|
"--control", str(control), "--watch-lua", "--headless",
|
|
"--frames", "10000000"], stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE, text=True, encoding="utf-8")
|
|
|
|
def collect():
|
|
for line in process.stderr:
|
|
logs.append(line)
|
|
|
|
reader = threading.Thread(target=collect, daemon=True)
|
|
reader.start()
|
|
|
|
def wait_for(marker, count=1):
|
|
deadline = time.monotonic() + 15
|
|
while time.monotonic() < deadline:
|
|
if "".join(logs).count(marker) >= count:
|
|
return
|
|
assert process.poll() is None, "".join(logs)
|
|
time.sleep(.025)
|
|
raise AssertionError((marker, "".join(logs)))
|
|
|
|
def send(sequence, command):
|
|
staging = root / "control.tmp"
|
|
staging.write_text(json.dumps({"sequence": sequence, "command": command}),
|
|
encoding="utf-8")
|
|
staging.replace(control)
|
|
|
|
try:
|
|
wait_for("LUA_START_A")
|
|
source.write_text("invalid Lua syntax !", encoding="utf-8")
|
|
wait_for("Lua reload rejected;")
|
|
time.sleep(.65)
|
|
assert "".join(logs).count("Lua reload rejected;") == 1, logs
|
|
assert "LUA_DESTROY_A" not in "".join(logs), logs
|
|
source.write_text(behavior("BAD", fail=True), encoding="utf-8")
|
|
wait_for("LUA_RELOAD_START_FAILED")
|
|
assert "LUA_DESTROY_A" not in "".join(logs), logs
|
|
source.write_text(behavior("B"), encoding="utf-8")
|
|
wait_for("LUA_START_B")
|
|
assert "".join(logs).count("LUA_DESTROY_A") == 1, logs
|
|
send(1, "reload-lua")
|
|
wait_for("LUA_START_B", count=2)
|
|
assert "".join(logs).count("LUA_DESTROY_B") == 1, logs
|
|
send(2, "stop")
|
|
process.wait(timeout=15)
|
|
reader.join(timeout=2)
|
|
assert process.returncode == 0, "".join(logs)
|
|
assert "".join(logs).count("LUA_DESTROY_B") == 2, logs
|
|
assert json.loads(process.stdout.read())["frames"] > 0
|
|
finally:
|
|
if process.poll() is None:
|
|
process.terminate()
|
|
try:
|
|
process.wait(timeout=3)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
process.wait(timeout=3)
|
|
reader.join(timeout=2)
|
|
print("Lua Player watched/explicit reload, startup rollback and shutdown logs passed")
|