Add opt-in heavy WASM budget tier
Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
# Opt-in heavy sandbox runtime tier; separate from the routine WASM contract.
|
||||
name: wasm-budget
|
||||
on:
|
||||
workflow_dispatch:
|
||||
env:
|
||||
CARGO_BUILD_JOBS: "1"
|
||||
CARGO_INCREMENTAL: "0"
|
||||
jobs:
|
||||
budget:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Rust
|
||||
run: |
|
||||
curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --target wasm32-unknown-unknown
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
- name: Actual WASM budget scenario
|
||||
run: nice -n 19 python3 tools/airdef.py test-budgets
|
||||
- name: Preserve machine-readable provenance
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: wasm-budget-provenance
|
||||
path: .horizon-cache/wasm-budget-report.json
|
||||
if-no-files-found: warn
|
||||
@@ -0,0 +1,278 @@
|
||||
# Opt-in heavy contract scenario. Run through `test-budgets` so the routine
|
||||
# `test-wasm` remains quick. This drives the real .hmod in Horizon's sandbox;
|
||||
# no GPU or rendered-frame correctness is implied by missing query replies.
|
||||
[[step]]
|
||||
ticks = 2
|
||||
frames = 2
|
||||
expect.commands = ["airdef"]
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy s400"
|
||||
expect.feedback = "С-400 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy patriot"
|
||||
expect.feedback = "Patriot развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy pantsir"
|
||||
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy gepard"
|
||||
expect.feedback = "Гепард развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy nasams"
|
||||
expect.feedback = "NASAMS развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy irist"
|
||||
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy tor"
|
||||
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy s400"
|
||||
expect.feedback = "С-400 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy patriot"
|
||||
expect.feedback = "Patriot развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy pantsir"
|
||||
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy gepard"
|
||||
expect.feedback = "Гепард развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy nasams"
|
||||
expect.feedback = "NASAMS развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy irist"
|
||||
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy tor"
|
||||
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy s400"
|
||||
expect.feedback = "С-400 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy patriot"
|
||||
expect.feedback = "Patriot развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy pantsir"
|
||||
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy gepard"
|
||||
expect.feedback = "Гепард развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy nasams"
|
||||
expect.feedback = "NASAMS развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy irist"
|
||||
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy tor"
|
||||
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy s400"
|
||||
expect.feedback = "С-400 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy patriot"
|
||||
expect.feedback = "Patriot развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy pantsir"
|
||||
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy gepard"
|
||||
expect.feedback = "Гепард развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy nasams"
|
||||
expect.feedback = "NASAMS развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy irist"
|
||||
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy tor"
|
||||
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy s400"
|
||||
expect.feedback = "С-400 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy patriot"
|
||||
expect.feedback = "Patriot развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy pantsir"
|
||||
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy gepard"
|
||||
expect.feedback = "Гепард развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy nasams"
|
||||
expect.feedback = "NASAMS развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy irist"
|
||||
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy tor"
|
||||
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy s400"
|
||||
expect.feedback = "С-400 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy patriot"
|
||||
expect.feedback = "Patriot развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy pantsir"
|
||||
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy gepard"
|
||||
expect.feedback = "Гепард развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy nasams"
|
||||
expect.feedback = "NASAMS развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy irist"
|
||||
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy tor"
|
||||
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy s400"
|
||||
expect.feedback = "С-400 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy patriot"
|
||||
expect.feedback = "Patriot развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy pantsir"
|
||||
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy gepard"
|
||||
expect.feedback = "Гепард развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy nasams"
|
||||
expect.feedback = "NASAMS развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy irist"
|
||||
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy tor"
|
||||
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy s400"
|
||||
expect.feedback = "С-400 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy patriot"
|
||||
expect.feedback = "Patriot развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy pantsir"
|
||||
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy gepard"
|
||||
expect.feedback = "Гепард развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy nasams"
|
||||
expect.feedback = "NASAMS развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy irist"
|
||||
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy tor"
|
||||
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy s400"
|
||||
expect.feedback = "С-400 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy patriot"
|
||||
expect.feedback = "Patriot развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy pantsir"
|
||||
expect.feedback = "Панцирь-С1 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy gepard"
|
||||
expect.feedback = "Гепард развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy nasams"
|
||||
expect.feedback = "NASAMS развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy irist"
|
||||
expect.feedback = "IRIS-T SLM развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy tor"
|
||||
expect.feedback = "Тор-М2 развёрнут (integrated, multi)"
|
||||
|
||||
[[step]]
|
||||
command = "airdef deploy s400"
|
||||
expect.feedback = "С-400 развёрнут (integrated, multi)"
|
||||
|
||||
# Two maximum supported waves are issued before simulation advances, so
|
||||
# their 160 airborne units overlap while 64 defensive systems track them.
|
||||
[[step]]
|
||||
command = "airdef wave mix 80 4000"
|
||||
|
||||
[[step]]
|
||||
command = "airdef wave kh101 80 4000"
|
||||
|
||||
[[step]]
|
||||
ticks = 600
|
||||
frames = 300
|
||||
|
||||
[[step]]
|
||||
command = "airdef clear"
|
||||
+105
-3
@@ -9,6 +9,7 @@
|
||||
python3 tools/airdef.py all # всё по порядку: fork, game, mod, install
|
||||
python3 tools/airdef.py check # совпадают ли форк, упаковщик, уровень API мода и ресурсы
|
||||
python3 tools/airdef.py test-wasm # собрать и прогнать настоящий .hmod в песочнице без GPU
|
||||
python3 tools/airdef.py test-budgets # тяжёлый WASM budget сценарий + JSON provenance
|
||||
|
||||
Переменные окружения: HORIZON_HOME (папка игры), HORIZON_RESOURCES (подготовленные ресурсы
|
||||
Minecraft 26.3), AIRDEF_GAME (готовый horizon-client вместо собранного).
|
||||
@@ -22,6 +23,7 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
@@ -132,8 +134,8 @@ def mod():
|
||||
"engine": engine_identity(), "engine_pin": engine_commit(),
|
||||
"wit_sha256": hashlib.sha256((FORK / "crates/horizon-mod-sdk/wit/horizon.wit").read_bytes()).hexdigest(),
|
||||
"artifact_sha256": hashlib.sha256(artifact.read_bytes()).hexdigest(),
|
||||
"airdef_dirty": bool(subprocess.check_output(["git", "status", "--porcelain", "--untracked-files=no"], cwd=ROOT)),
|
||||
"airdef_diff_sha256": hashlib.sha256(subprocess.check_output(["git", "diff", "--binary", "HEAD"], cwd=ROOT)).hexdigest()}
|
||||
"airdef_dirty": bool(subprocess.check_output(["git", "-c", "submodule.horizon.ignore=all", "status", "--porcelain", "--untracked-files=no"], cwd=ROOT)),
|
||||
"airdef_diff_sha256": hashlib.sha256(subprocess.check_output(["git", "-c", "submodule.horizon.ignore=all", "diff", "--binary", "HEAD"], cwd=ROOT)).hexdigest()}
|
||||
artifact.with_name("build-identity.json").write_text(json.dumps(identity, indent=2) + "\n")
|
||||
|
||||
|
||||
@@ -155,6 +157,106 @@ def test_wasm():
|
||||
cwd=ROOT / "airdef-mod", env=build_env)
|
||||
|
||||
|
||||
def test_budgets():
|
||||
"""Run the opt-in heavy scenario and always write a machine-readable report."""
|
||||
report_path = Path(os.environ.get("AIRDEF_BUDGET_REPORT") or
|
||||
ROOT / ".horizon-cache" / "wasm-budget-report.json")
|
||||
report = {
|
||||
"schema": 1,
|
||||
"command": "test-budgets",
|
||||
"scenario": "tests/budgets/wasm_budget.toml",
|
||||
"status": "failed",
|
||||
"exit_code": None,
|
||||
"scenario_counts": {"scripts": 1, "steps": None, "raids": 2,
|
||||
"max_units_per_raid": 80, "raid_units": 160,
|
||||
"air_defense_units": None, "layout_unit_limit": None,
|
||||
"frames": 300, "ticks": 602},
|
||||
"runtime_limits": {"source": "horizon/crates/horizon-mods/src/runtime.rs",
|
||||
"metering": "source_configured; measured_per_scenario: unavailable"},
|
||||
}
|
||||
|
||||
def git(*args, text=True):
|
||||
return subprocess.check_output(["git", "-c", "submodule.horizon.ignore=all", *args], cwd=ROOT,
|
||||
text=text, stderr=subprocess.DEVNULL)
|
||||
|
||||
try:
|
||||
report["airdef"] = {
|
||||
"sha": git("rev-parse", "HEAD").strip(),
|
||||
"dirty": bool(git("status", "--porcelain", "--untracked-files=no").strip()),
|
||||
"diff_sha256": hashlib.sha256(git("diff", "--binary", "HEAD", text=False)).hexdigest(),
|
||||
}
|
||||
report["engine"] = {"pin": engine_commit(), "identity": engine_identity()}
|
||||
wit = FORK / "crates" / "horizon-mod-sdk" / "wit" / "horizon.wit"
|
||||
report["api_level"] = fork_api()[0] if fork_api() else None
|
||||
report["wit_sha256"] = hashlib.sha256(wit.read_bytes()).hexdigest() if wit.is_file() else None
|
||||
runtime = (FORK / "crates" / "horizon-mods" / "src" / "runtime.rs").read_text()
|
||||
layout = (ROOT / "airdef-mod" / "src" / "layout.rs").read_text()
|
||||
limit = re.search(r"pub const MAX_UNITS: usize = (\d+);", layout)
|
||||
report["scenario_counts"]["layout_unit_limit"] = int(limit.group(1)) if limit else None
|
||||
memory = re.search(r"const MEMORY: usize = (\d+) << (\d+);", runtime)
|
||||
report["runtime_limits"].update({
|
||||
"memory_bytes": int(memory.group(1)) << int(memory.group(2)) if memory else None,
|
||||
"fuel_per_call": {name.lower(): int(value) for name, value in
|
||||
re.findall(r"const FUEL_(INIT|TICK|FRAME|EVENT): u64 = ([\d_]+);", runtime)
|
||||
for value in [value.replace("_", "")]},
|
||||
"wasmtime_component_model": bool(re.search(r"wasm_component_model\(true\)", runtime)),
|
||||
"wasmtime_fuel_metering": bool(re.search(r"consume_fuel\(true\)", runtime)),
|
||||
})
|
||||
lock_text = (ROOT / "horizon" / "Cargo.lock").read_text()
|
||||
wasmtime = re.search(r'\[\[package\]\]\s*name = "wasmtime"\s*version = "([^"]+)"', lock_text)
|
||||
report["wasmtime_version"] = wasmtime.group(1) if wasmtime else None
|
||||
fixture = ROOT / "airdef-mod" / "tests" / "budgets" / "wasm_budget.toml"
|
||||
report["scenario_counts"]["steps"] = len(re.findall(r"(?m)^\[\[step\]\]$", fixture.read_text()))
|
||||
report["scenario_counts"]["air_defense_units"] = len(re.findall(r'(?m)^command = "airdef deploy [^"]+"', fixture.read_text()))
|
||||
report["scenario_sha256"] = hashlib.sha256(fixture.read_bytes()).hexdigest()
|
||||
|
||||
mod()
|
||||
artifact = hmod()
|
||||
report["artifact_sha256"] = hashlib.sha256(artifact.read_bytes()).hexdigest()
|
||||
identity_path = artifact.with_name("build-identity.json")
|
||||
identity = json.loads(identity_path.read_text())
|
||||
identity_checks = {
|
||||
"engine_pin": identity.get("engine_pin") == report["engine"]["pin"],
|
||||
"engine_identity": identity.get("engine") == report["engine"]["identity"],
|
||||
"airdef_sha": identity.get("airdef") == report["airdef"]["sha"],
|
||||
"airdef_dirty": identity.get("airdef_dirty") == report["airdef"]["dirty"],
|
||||
"airdef_diff_sha256": identity.get("airdef_diff_sha256") == report["airdef"]["diff_sha256"],
|
||||
"artifact_sha256": identity.get("artifact_sha256") == report["artifact_sha256"],
|
||||
"wit_sha256": identity.get("wit_sha256") == report["wit_sha256"],
|
||||
}
|
||||
report["build_identity"] = {"path": str(identity_path), "checks": identity_checks,
|
||||
"matches": all(identity_checks.values())}
|
||||
if not report["build_identity"]["matches"]:
|
||||
raise RuntimeError("build identity is stale or does not match the tested artifact: " +
|
||||
", ".join(k for k, ok in identity_checks.items() if not ok))
|
||||
build_env = dict(os.environ)
|
||||
build_env["PATH"] = str(Path(cargo()).parent) + os.pathsep + build_env.get("PATH", "")
|
||||
fixture = ROOT / "airdef-mod" / "tests" / "budgets" / "wasm_budget.toml"
|
||||
with tempfile.TemporaryDirectory(prefix="airdef-wasm-budget-") as temp:
|
||||
isolated = Path(temp)
|
||||
(isolated / "tests").mkdir()
|
||||
shutil.copy2(fixture, isolated / "tests" / "wasm_budget.toml")
|
||||
command = [cargo(), "run", "--locked", "--manifest-path", FORK / "Cargo.toml",
|
||||
"-p", "cargo-horizon-mod", "--", "horizon-mod", "test", "--hmod", artifact, "budget"]
|
||||
print("+", " ".join(str(c) for c in command), flush=True)
|
||||
result = subprocess.run([str(c) for c in command], cwd=isolated, env=build_env,
|
||||
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
|
||||
print(result.stdout, end="", flush=True)
|
||||
report["exit_code"] = result.returncode
|
||||
report["status"] = "passed" if result.returncode == 0 else "failed"
|
||||
report["output"] = result.stdout[-12000:]
|
||||
except Exception as exc:
|
||||
report["error"] = f"{type(exc).__name__}: {exc}"
|
||||
except SystemExit as exc:
|
||||
report["error"] = f"SystemExit: {exc.code}"
|
||||
finally:
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
|
||||
print(f"Budget report: {report_path}", flush=True)
|
||||
if report["status"] != "passed":
|
||||
sys.exit(f"WASM budget scenario failed; see {report_path}")
|
||||
|
||||
|
||||
def install():
|
||||
home = game_home()
|
||||
mods = home / "horizon-mods"
|
||||
@@ -298,7 +400,7 @@ def main():
|
||||
if len(sys.argv) < 2:
|
||||
sys.exit(__doc__)
|
||||
cmd, rest = sys.argv[1], sys.argv[2:]
|
||||
steps = {"fork": fork, "game": game, "mod": mod, "install": install, "check": check, "test-wasm": test_wasm}
|
||||
steps = {"fork": fork, "game": game, "mod": mod, "install": install, "check": check, "test-wasm": test_wasm, "test-budgets": test_budgets}
|
||||
if cmd == "play":
|
||||
play(rest)
|
||||
elif cmd == "all":
|
||||
|
||||
Reference in New Issue
Block a user