makepad/libs/relax/compiler/tests/check_watch/run.py
Admin b22a714840 makepad run: the Rust JIT (libs/relax) and makepad-remote
The JIT that runs makepad apps from source: lexer, front end, type checker,
lazy borrow checker (Gandalf; `--check-first` opts in), codegen, session and
warm/cold caches keyed by compiler stamp, provided crates bound late,
startup images, live patching. User-facing name is `makepad run` (binary
`makepad`, MAKEPAD_RUN_* env, ~/.makepad-run); libs/relax stays the internal
library name. `makepad check` reports rustc-style diagnostics with rustc
error codes and checks a whole workspace (`--workspace`). Runs on macOS,
Linux, Windows and Android. makepad-remote: one tool schema for apps and the
WM (app/1): grabs, captures, traces, frame profiles and tweaks.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 12:26:21 +02:00

95 lines
5 KiB
Python

#!/usr/bin/env python3
"""Five warm edits against isolated one-shot references; original app is read-only.
Spec is a JSON array of five {name, replace: [old,new]} or {name, append: text}.
Each edit starts from the initial text; {name: 'revert'} restores it.
"""
import json, os, pathlib, queue, re, shutil, subprocess, sys, tempfile, threading, time
relax, app, pkg, edit, spec = sys.argv[1:]
relax, app, edit = map(lambda p: pathlib.Path(p).resolve(), (relax, app, edit))
steps = json.loads(pathlib.Path(spec).read_text())
assert len(steps) == 5, 'expected body error, revert, signature, new fn, unresolved name'
tmp = pathlib.Path(tempfile.mkdtemp(prefix='relax-check-watch-'))
relative = edit.relative_to(app)
for name in ('warm', 'reference'):
dst = tmp / name
shutil.copytree(app, dst, ignore=shutil.ignore_patterns('target', '.git'))
manifest = dst / 'Cargo.toml'
source = manifest.read_text()
source = re.sub(r'(\bpath\s*=\s*")([^"]+)(")', lambda m: m[0] if (app / m[2]).resolve().is_relative_to(app) else m[1] + str((app / m[2]).resolve()) + m[3], source)
if '[workspace]' not in source:
source += '\n[workspace]\n'
manifest.write_text(source)
base = (tmp / 'warm' / relative).read_text()
env = dict(os.environ, MAKEPAD_RUN_STD=str(pathlib.Path(__file__).resolve().parents[2] / 'std'),
MAKEPAD_RUN_CACHE_DIR=str(tmp / 'target'), MAKEPAD_RUN_CACHE='off', CARGO_NET_OFFLINE='true')
# The unit-graph resolver must not fetch missing registry packages in this gate.
env['MAKEPAD_RUN_OFFLINE'] = '1'
err = open(tmp / 'watch.stderr', 'w')
child = subprocess.Popen([str(relax), 'check', '--watch', '--json', pkg], cwd=tmp / 'warm', env=env,
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=err, text=True, bufsize=1)
q = queue.Queue()
def read():
for line in child.stdout: q.put(line)
q.put(None)
threading.Thread(target=read, daemon=True).start()
def batch():
rows = []
deadline = time.monotonic() + 180
while True:
line = q.get(timeout=max(0.01, deadline-time.monotonic()))
assert line is not None, f'watch exited {child.poll()}; see {tmp}'
row = json.loads(line)
if row.get('type') == 'summary': return rows, row
rows.append(row)
def normalized(rows, root):
# Also normalize suggestion replacement paths and any source paths in notes/messages.
return sorted(json.dumps(row, sort_keys=True).replace(str(root), '<app>') for row in rows)
def reference(text):
(tmp / 'reference' / relative).write_text(text)
r = subprocess.run([str(relax), 'check', '--json', pkg], cwd=tmp / 'reference', env=env,
capture_output=True, text=True, timeout=180)
(tmp / 'reference.stderr').write_text(r.stderr)
assert r.returncode in (0, 1), (r.returncode, r.stderr, tmp)
rows = [json.loads(line) for line in r.stdout.splitlines()]
assert rows and rows[-1].get('type') == 'summary', (r.stdout, r.stderr, tmp)
assert rows[-1]['errors'] == len(rows)-1
return rows[:-1]
def compare(rows, ref, label):
got, want = normalized(rows, tmp/'warm'), normalized(ref, tmp/'reference')
if got != want:
(tmp / (label+'.actual.json')).write_text('\n'.join(got))
(tmp / (label+'.expected.json')).write_text('\n'.join(want))
raise AssertionError(f'{label}: diagnostics differ; artifacts {tmp}')
try:
rows, summary = batch()
compare(rows, reference(base), 'initial')
baseline = normalized(rows, tmp/'warm')
print('initial: ' + json.dumps(summary), flush=True)
slow = []
for n, step in enumerate(steps):
text = base
if 'replace' in step:
old, new = step['replace']
assert old in text, (step['name'], 'anchor not found')
text = text.replace(old, new, 1)
text += step.get('append', '')
child.stdin.write(json.dumps({'method':'didChange','path':str(tmp/'warm'/relative),'text':text})+'\n')
child.stdin.flush()
rows, summary = batch()
assert summary['errors'] == len(rows)
(tmp / (str(n)+'-'+step['name']+'.summary.json')).write_text(json.dumps(summary))
print(f"{step['name']}: {summary['ms']:.3f} ms, {summary['rechecked']} rechecked, RSS {summary['rss_before_trim_mb']:.1f} -> {summary['rss_mb']:.1f} MB", flush=True)
compare(rows, reference(text), str(n)+'-'+step['name'])
if n == 0: assert normalized(rows, tmp/'warm') != baseline, 'body-error edit did not change diagnostics'
if n < 2 and summary['ms'] >= 100: slow.append((step['name'], summary['ms']))
child.stdin.write('{"method":"shutdown"}\n'); child.stdin.flush()
assert child.wait(timeout=10) == 0
assert not slow, f'all diagnostics matched, but body edits exceeded 100 ms: {slow}; artifacts {tmp}'
print('PASS: all five edits equal one-shot diagnostics; artifacts ' + str(tmp))
finally:
if child.poll() is None:
child.terminate() # This child PID only.
try: child.wait(timeout=5)
except subprocess.TimeoutExpired: child.kill(); child.wait()
err.close()