makepad/libs/relax/compiler/tests/live_app/run.py
2026-10-25 11:49:40 +01:00

107 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""The live driver inside `makepad run` (a working tree runs live, reload.rs start_watcher), end to end.
usage: run.py --makepad <makepad binary> [--warm] (MAKEPAD_RUN_STD in the environment)
Copies pkg/ to a temporary dir, runs it (a working tree: live) and edits src/helper.rs while it runs:
1. body edit: value() returns 2 -> stdout shows 2, stderr "patched"
2. type error in value() -> stderr "NOT applied" + the error; output stays 2
3. struct field added to Conf (a Copy struct) -> a Restart rung: makepad restarts for real on its
own (cargo first checks the makepad binary is
current; up to 300 s if it has to build): a
state-preserving restart, the program gets its tick
count back and runs the new code (value 1 + extra 5 = 6)
4. the struct back + value() returns 999 -> the program sees 999 and exits
Each step waits up to 20 s. Prints PASS/FAIL per step; exit 1 on any FAIL.
"""
import os, shutil, subprocess, sys, tempfile, threading, time
HERE = os.path.dirname(os.path.abspath(__file__))
relax = os.path.abspath(sys.argv[sys.argv.index("--makepad") + 1])
tmp = tempfile.mkdtemp(prefix="live-app-")
pkg = os.path.join(tmp, "pkg")
shutil.copytree(os.path.join(HERE, "pkg"), pkg)
helper = os.path.join(pkg, "src", "helper.rs")
base = open(helper).read()
# the live driver writes this file once it watches the sources (MAKEPAD_RUN_LIVE_READY)
ready = os.path.join(tmp, "live-ready")
# --warm: a live session from a warm cache. A first live run (one tick, no edit) fills a fresh
# cache; the edits below then run on a program restored from it (collected graph), not parsed.
warm = "--warm" in sys.argv
if warm:
env = dict(os.environ, MAKEPAD_RUN_LIVE_READY=ready, MAKEPAD_RUN_CACHE_DIR=os.path.join(tmp, "cache"), MAKEPAD_RUN_TARGET_DIR=os.path.join(tmp, "target"))
fill = subprocess.run([relax, "run", "--quiet", "relax-live-app"], cwd=pkg, env=dict(env, LIVE_APP_TICKS="1"), capture_output=True, text=True, timeout=600)
print("%s warm-up run fills the cache (exit %d)" % ("PASS" if fill.returncode == 0 else "FAIL", fill.returncode))
if fill.returncode != 0:
print(fill.stderr[-3000:]); sys.exit(1)
else:
env = dict(os.environ, MAKEPAD_RUN_LIVE_READY=ready, MAKEPAD_RUN_CACHE="0")
if os.path.exists(ready):
os.remove(ready) # written by the runs before this one
p = subprocess.Popen([relax, "run", "--verbose" if warm else "--quiet", "relax-live-app"], cwd=pkg, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
out, err = [], []
def pump(f, acc):
for line in f:
acc.append(line.rstrip("\n"))
threading.Thread(target=pump, args=(p.stdout, out), daemon=True).start()
threading.Thread(target=pump, args=(p.stderr, err), daemon=True).start()
def wait(pred, secs=20):
t = time.time()
while time.time() - t < secs:
if pred():
return True
time.sleep(0.05)
return False
def last_value():
for l in reversed(out):
if l.startswith("out tick "):
return int(l.split()[3])
return None
def save(text):
# a new mtime even within the same second: write, then bump the time
with open(helper, "w") as f:
f.write(text)
st = os.stat(helper)
os.utime(helper, ns=(st.st_atime_ns, st.st_mtime_ns + 1_000_000))
fails = 0
def step(name, ok):
global fails
print("%s %s" % ("PASS" if ok else "FAIL", name))
if not ok:
fails += 1
step("starts", wait(lambda: last_value() == 1 and os.path.exists(ready), 120))
if warm:
step("live start restored the collected graph", wait(lambda: any(l.startswith("makepad-run collected: restored") for l in err)))
t = time.time()
save(base.replace("conf().base\n", "conf().base + 1\n"))
ok = wait(lambda: last_value() == 2)
step("body edit applies (%.0f ms save -> new output)" % ((time.time() - t) * 1e3), ok and any("patched" in l for l in err))
n_err = len(err)
save(base.replace("conf().base\n", "conf().base + \"x\"\n"))
ok = wait(lambda: any("NOT applied" in l for l in err[n_err:]))
time.sleep(0.3)
step("type error refused, app keeps running the previous code", ok and last_value() == 2)
n_err = len(err)
n_before = max((int(l.split()[2]) for l in out if l.startswith("out tick ")), default=0)
layout = base.replace(" pub base: u64,\n", " pub base: u64,\n pub extra: u8,\n").replace("Conf { base: 1 }", "Conf { base: 1, extra: 5 }").replace("conf().base\n", "conf().base + conf().extra as u64\n")
save(layout)
restarting = wait(lambda: any("restarting makepad" in l for l in err[n_err:]))
step("layout edit restarts makepad on its own (no `session relaunch`)", restarting)
ok = wait(lambda: last_value() == 6, 300)
restored = [int(l.split()[2]) for l in out if l.startswith("out restored ")]
step("layout edit: state-preserving restart (restored tick %s, before %d), new code runs" % (restored[-1] if restored else None, n_before), ok and bool(restored) and restored[-1] >= n_before)
save(layout.replace("conf().base + conf().extra as u64\n", "999\n"))
ok = wait(lambda: p.poll() is not None)
step("final edit applies, program exits", ok and last_value() == 999)
if p.poll() is None:
p.kill()
if fails:
print("---- stderr"); print("\n".join(err[-40:]))
shutil.rmtree(tmp, ignore_errors=True)
print("live app: %d fail" % fails)
sys.exit(1 if fails else 0)