Add optional Lua scripting module, examples, and validation
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"""CPU-only schema-export and Player validation contracts for optional Lua."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--exporter", required=True)
|
||||
parser.add_argument("--player")
|
||||
parser.add_argument("--disabled", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def run(executable, *arguments, success=True):
|
||||
result = subprocess.run([executable, *map(str, arguments)], capture_output=True,
|
||||
text=True, encoding="utf-8", timeout=20)
|
||||
assert (result.returncode == 0) == success, (result.stdout, result.stderr)
|
||||
return result
|
||||
|
||||
|
||||
def behavior(type_id="test.lua.cli", callback='error("LUA_CALLBACK_MUST_NOT_RUN")'):
|
||||
return f'''local behavior = faset.behavior {{
|
||||
id = "{type_id}", version = 1, name = "CLI behavior",
|
||||
fields = {{ speed = {{ type = "number", default = 3, min = 0 }} }}
|
||||
}}
|
||||
function behavior:on_start() {callback} end
|
||||
return behavior
|
||||
'''
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix="faset-lua-cli-") as temporary:
|
||||
root = Path(temporary) / "Lua Café 世界"
|
||||
scripts = root / "Scripts"
|
||||
scripts.mkdir(parents=True)
|
||||
manifest = root / "project.faset.json"
|
||||
manifest.write_text(json.dumps({"format": "faset.project", "version": 1}), encoding="utf-8")
|
||||
native = json.loads(run(args.exporter).stdout)
|
||||
assert json.loads(run(args.exporter, "--project", root).stdout) == native
|
||||
manifest.write_text(json.dumps({"format": "faset.project", "version": 1,
|
||||
"scripting": {"lua": {"scripts": ["Scripts/cli.lua"]}}}),
|
||||
encoding="utf-8")
|
||||
source = scripts / "cli.lua"
|
||||
source.write_text(behavior(), encoding="utf-8")
|
||||
if args.disabled:
|
||||
result = run(args.exporter, "--project", root, success=False)
|
||||
assert "without Lua support" in result.stderr, result.stderr
|
||||
if args.player:
|
||||
run(args.player, "--help")
|
||||
print("Lua-disabled schema exporter accepts C++ projects and rejects Lua clearly")
|
||||
else:
|
||||
exported = root / "schema.json"
|
||||
run(args.exporter, "--project", root, "--output", exported)
|
||||
merged = json.loads(exported.read_text(encoding="utf-8"))
|
||||
assert merged["format"] == "faset.schema" and merged["version"] == 1
|
||||
by_id = {item["id"]: item for item in merged["types"]}
|
||||
assert len(by_id) == len(native["types"]) + 1
|
||||
assert by_id["test.lua.cli"]["fields"]["speed"]["default"] == 3
|
||||
for item in native["types"]:
|
||||
assert by_id[item["id"]] == item
|
||||
|
||||
# Lua lifecycle errors do not execute in the schema exporter or --validate.
|
||||
scene = root / "scene.scene.json"
|
||||
scene.write_text(json.dumps({
|
||||
"format": "faset.scene", "version": 1, "dimension": 2,
|
||||
"id": "lua-cli-scene", "name": "Lua CLI", "instances": [],
|
||||
"entities": [{"id": "lua-object", "name": "Lua", "parent": None,
|
||||
"components": [{"id": "lua-behavior", "type": "test.lua.cli",
|
||||
"version": 1, "fields": {"speed": 3}}]}]
|
||||
}), encoding="utf-8")
|
||||
if args.player:
|
||||
for project_arguments in [("--project", root), ()]:
|
||||
result = run(args.player, "--scene", scene, "--validate", *project_arguments)
|
||||
assert json.loads(result.stdout)["validated"]
|
||||
assert "LUA_CALLBACK_MUST_NOT_RUN" not in result.stderr
|
||||
valid_scene = scene.read_text(encoding="utf-8")
|
||||
invalid_scene = json.loads(valid_scene)
|
||||
for invalid_speed in ["fast", -1]:
|
||||
invalid_scene["entities"][0]["components"][0]["fields"]["speed"] = invalid_speed
|
||||
scene.write_text(json.dumps(invalid_scene), encoding="utf-8")
|
||||
result = run(args.player, "--scene", scene, "--validate", "--project", root,
|
||||
success=False)
|
||||
assert "speed" in result.stderr, result.stderr
|
||||
assert "LUA_CALLBACK_MUST_NOT_RUN" not in result.stderr, result.stderr
|
||||
scene.write_text(valid_scene, encoding="utf-8")
|
||||
run(args.player, "--scene", scene, "--validate", "--watch-lua", success=False)
|
||||
|
||||
# A failed schema candidate never overwrites a previously published file.
|
||||
previous = exported.read_bytes()
|
||||
source.write_text("this is invalid Lua!", encoding="utf-8")
|
||||
result = run(args.exporter, "--project", root, "--output", exported, success=False)
|
||||
assert "cli.lua" in result.stderr, result.stderr
|
||||
assert exported.read_bytes() == previous
|
||||
source.write_text(behavior("faset.transform"), encoding="utf-8")
|
||||
run(args.exporter, "--project", root, success=False)
|
||||
if native["types"]:
|
||||
source.write_text(behavior(native["types"][0]["id"]), encoding="utf-8")
|
||||
run(args.exporter, "--project", root, success=False)
|
||||
source.write_text("while true do end", encoding="utf-8")
|
||||
run(args.exporter, "--project", root, success=False)
|
||||
source.write_text(behavior(), encoding="utf-8")
|
||||
assert json.loads(run(args.exporter, "--project", root).stdout) == merged
|
||||
print("Lua schema merge, lifecycle-free validation, failure isolation and budgets passed")
|
||||
Reference in New Issue
Block a user