Add optional Lua scripting module, examples, and validation
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user