Files
Faset_Engine/tests/lua_player_reload_test.py

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")