Add optional Lua scripting module, examples, and validation

This commit is contained in:
emil28092005
2026-09-18 11:28:50 +03:00
parent 06210aac23
commit 5a67735ff4
45 changed files with 3887 additions and 92 deletions
+99
View File
@@ -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")