126 lines
5.4 KiB
Python
126 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""LIVEICE: a refused edit then the applied edit of a fn with closures, inside `makepad run` (the live
|
|
driver of a working tree), with the closures and their generic instances first built afterwards on
|
|
another thread. The user's Amp parked its worker thread on "internal compiler error in
|
|
{closure#N}@S: side table: no entry" after exactly this sequence.
|
|
|
|
usage: run.py --makepad <makepad binary> [--warm] (MAKEPAD_RUN_STD in the environment)
|
|
|
|
Copies pkg/ to a temporary dir, runs it and edits src/helper.rs while it runs:
|
|
1. `parse` calls `first_end` before it exists -> "NOT applied" (E0425), the app keeps running
|
|
2. `first_end` added above `parse`, parse uses it -> "patched"
|
|
3. the file `go` makes the worker answer requests -> "out song" lines with the v2 parse result
|
|
(closures of the replaced version built now),
|
|
no internal compiler error
|
|
--warm: the session runs from a warm cache (a first run fills it).
|
|
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])
|
|
warm = "--warm" in sys.argv
|
|
tmp = tempfile.mkdtemp(prefix="live-closure-")
|
|
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()
|
|
FIRST_END = '''fn first_end(text: &str, start: usize) -> Option<usize> {
|
|
let mut depth = 0usize;
|
|
for (i, b) in text.bytes().enumerate().skip(start) {
|
|
match b {
|
|
b'{' => depth += 1,
|
|
b'}' => {
|
|
depth -= 1;
|
|
if depth == 0 {
|
|
return Some(i);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
'''
|
|
v1 = base.replace("let end = answer.rfind('}')", "let end = first_end(answer, start)")
|
|
assert v1 != base
|
|
v2 = v1.replace("pub fn parse(", FIRST_END + "pub fn parse(", 1)
|
|
ANSWER = 'x {"title": "T%d", "steps": 8} {"title": "Z"}'
|
|
def expect(n, version):
|
|
a = ANSWER % (n % 3)
|
|
end = a.index("}") + 8 if version == 2 else a.rindex("}")
|
|
return "out song %d T%d:%d" % (n, n % 3, end)
|
|
|
|
# minimal ahead: no speculative builds before the edits (the closures stay unbuilt until `go`, as
|
|
# they were in the user's Amp; balanced ahead builds them early and hides the bug)
|
|
base_env = dict(os.environ, MAKEPAD_RUN_AHEAD="minimal")
|
|
if warm:
|
|
env = dict(base_env, MAKEPAD_RUN_CACHE_DIR=os.path.join(tmp, "cache"), MAKEPAD_RUN_TARGET_DIR=os.path.join(tmp, "target"))
|
|
open(os.path.join(pkg, "go"), "w").close()
|
|
open(os.path.join(pkg, "stop"), "w").close()
|
|
fill = subprocess.run([relax, "run", "--quiet", "relax-live-closure-app"], cwd=pkg, env=env, capture_output=True, text=True, timeout=600)
|
|
os.remove(os.path.join(pkg, "go")); os.remove(os.path.join(pkg, "stop"))
|
|
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(base_env, MAKEPAD_RUN_CACHE="0")
|
|
p = subprocess.Popen([relax, "run", "--verbose", "relax-live-closure-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=30):
|
|
t = time.time()
|
|
while time.time() - t < secs:
|
|
if pred():
|
|
return True
|
|
time.sleep(0.05)
|
|
return False
|
|
|
|
def save(text):
|
|
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
|
|
|
|
def ticks():
|
|
return sum(1 for l in out if l.startswith("out tick "))
|
|
step("starts", wait(lambda: ticks() > 2 and any("watching" in l for l in err), 180))
|
|
n_err = len(err)
|
|
save(v1)
|
|
ok = wait(lambda: any("NOT applied" in l for l in err[n_err:]))
|
|
step("call to a missing fn refused", ok)
|
|
n_err = len(err)
|
|
save(v2)
|
|
ok = wait(lambda: any("patched" in l for l in err[n_err:]))
|
|
step("fn added and used: applied", ok)
|
|
n_out = len(out)
|
|
open(os.path.join(pkg, "go"), "w").close()
|
|
ok = wait(lambda: sum(1 for l in out[n_out:] if l.startswith("out song ") or l.startswith("out failed ")) >= 5)
|
|
songs = [l for l in out[n_out:] if l.startswith("out song ") or l.startswith("out failed ")]
|
|
bad = [l for l in songs if l != expect(int(l.split()[2]), 2)]
|
|
step("worker runs the new parse (%d songs, %d wrong%s)" % (len(songs), len(bad), (": " + bad[0]) if bad else ""), ok and not bad)
|
|
ice = [l for l in err if "internal compiler error" in l or "side table" in l]
|
|
step("no internal compiler error%s" % ((": " + ice[0][:300]) if ice else ""), not ice)
|
|
open(os.path.join(pkg, "stop"), "w").close()
|
|
ok = wait(lambda: p.poll() is not None)
|
|
step("program exits", ok and p.returncode == 0)
|
|
if p.poll() is None:
|
|
p.kill()
|
|
if fails:
|
|
print("---- stderr"); print("\n".join(err[-60:]))
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
print("live closure app: %d fail" % fails)
|
|
sys.exit(1 if fails else 0)
|