makepad/libs/relax/compiler/tests/serve/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

166 lines
8.8 KiB
Python

#!/usr/bin/env python3
"""Loopback protocol and CLI integration. Fixture copies and logs survive for inspection."""
import concurrent.futures, json, os, pathlib, shutil, signal, socket, subprocess, sys, tempfile, time
relax = pathlib.Path(sys.argv[1]).resolve()
here = pathlib.Path(__file__).resolve().parent
work = pathlib.Path(tempfile.mkdtemp(prefix='relax-serve-'))
fixture = work / 'fixture'
shutil.copytree(here / 'fixture', fixture)
cache = work / 'cache'
env = dict(os.environ, MAKEPAD_RUN_CACHE_DIR=str(cache), MAKEPAD_RUN_CACHE='off',
MAKEPAD_RUN_STD=str(here.parents[1] / 'std'),
CARGO_NET_OFFLINE='true', MAKEPAD_RUN_OFFLINE='1')
source = fixture / 'main.rs'
original = source.read_text()
fixed = original.replace('Missing', 'i32')
log = open(work / 'serve.log', 'w')
child = subprocess.Popen([str(relax), 'serve', '--workspace', str(fixture)], env=env, stdout=log, stderr=log)
endpoint = None
def await_endpoint():
deadline = time.monotonic() + 180
while time.monotonic() < deadline:
entries = list(cache.glob('serve/*/endpoint'))
if entries and len(entries[0].read_text().split()) == 3:
return entries[0]
assert child.poll() is None, (child.returncode, (work/'serve.log').read_text())
time.sleep(.02)
raise AssertionError('endpoint timeout')
class Client:
def __init__(self, token=None):
port, self.token, _ = endpoint.read_text().split()
if token is not None: self.token = token
self.socket = socket.create_connection(('127.0.0.1', int(port)), timeout=180)
self.file = self.socket.makefile('rwb')
self.id = 0
def call(self, method, **params):
self.id += 1
self.file.write((json.dumps(dict(id=self.id, token=self.token, method=method, params=params))+'\n').encode())
self.file.flush()
reply = json.loads(self.file.readline())
assert reply['id'] == self.id, reply
assert 'error' not in reply, reply
return reply['result']
def close(self):
self.file.close(); self.socket.close()
def cli(*args):
p = subprocess.run([str(relax), *args], cwd=fixture, env=env, text=True, capture_output=True, timeout=180)
return p, [json.loads(s) for s in p.stdout.splitlines()]
def stable(rows):
rows = [dict(r) for r in rows]
rows[-1]['ms'] = 0
return rows
def position(anchor, token):
lines = fixed.splitlines()
line = next(i for i,l in enumerate(lines) if anchor in l)
return dict(path=str(source), line=line+1, col=lines[line].index(token)+1)
try:
endpoint = await_endpoint()
assert len(endpoint.parent.name) == 16
if os.name != 'nt': assert endpoint.stat().st_mode & 0o777 == 0o600
duplicate = subprocess.run([str(relax), 'serve'], cwd=fixture, env=env, capture_output=True, text=True, timeout=20)
assert duplicate.returncode == 0 and duplicate.stdout == endpoint.read_text(), duplicate
client = Client()
assert client.call('status')['packages'] == []
initial = client.call('check', pkg='serve_fixture')
print('initial warm check:', initial['summary'], flush=True)
one, rows = cli('check', '--no-serve', '--json', 'serve_fixture')
assert one.returncode == 1 and not one.stderr, one.stderr
assert initial['diagnostics'] == rows[:-1], (initial, rows)
assert any(d['code'] == 'E0412' for d in initial['diagnostics'])
auto, auto_rows = cli('check', '--json', 'serve_fixture')
assert auto.returncode == one.returncode and stable(auto_rows) == stable(rows), (auto, rows)
client.call('didChange', path=str(source), text=fixed)
clean = client.call('check', pkg='serve_fixture')
print('fixed warm check:', clean['summary'], flush=True)
assert clean['diagnostics'] == [], clean
for anchor, token, kind, typ in [('let field', 'local', 'local', 'Counter'), ('let field', 'value', 'field', 'i32'), ('let call', 'read', 'fn', 'i32')]:
value = client.call('hover', **position(anchor,token))
assert value and value['kind'] == kind and typ in value['type'], (token,value)
print('hover', token, value, flush=True)
defs = client.call('definition', **position('let _result','helper'))
assert defs and defs[0]['line'] == 13, defs
schema = client.call('schema', pkg='serve_fixture', path='crate::Counter')
assert schema['kind'] == 'struct' and 'documented counter' in schema['docs'] and 'Used by' in schema['docs'], schema
assert schema['fields'][0]['type'] == 'i32' and 'Current count' in schema['fields'][0]['docs'], schema
assert any(m['name'] == 'read' and 'Read the count' in m['docs'] for m in schema['methods']), schema
assert client.call('status')['packages'][0]['items'] >= 3
other = client.call('schema', pkg='serve_other', path='Other')
assert other['traits'] and any(m['name']=='read' for m in other['methods']), other
variants = client.call('schema', pkg='serve_other', path='Choice')['variants']
assert [v['name'] for v in variants] == ['Empty','Value','Named'], variants
assert variants[1]['fields'][0]['type'] == 'u32'
assert len(client.call('status')['packages']) == 2
nested = subprocess.run([str(relax),'check','--json','serve_other'],cwd=fixture/'other',env=env,capture_output=True,text=True,timeout=180)
assert nested.returncode == 0 and len(list(cache.glob('serve/*/endpoint'))) == 1, nested
# Loading a second package must not invalidate the first package's editor state.
assert client.call('hover', **position('let field','value'))['type'] == 'i32'
# Several connections share the same package writer.
def parallel(_):
c = Client()
try: return c.call('check', pkg='serve_fixture')['diagnostics']
finally: c.close()
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool:
assert list(pool.map(parallel, range(4))) == [[],[],[],[]]
# Unsaved content survives disk edits; text:null restores the disk version.
source.write_text(original + '\n')
time.sleep(.25)
assert client.call('check', pkg='serve_fixture')['diagnostics'] == []
client.call('didChange', path=str(source), text=None)
assert client.call('check', pkg='serve_fixture')['diagnostics']
# Watcher pushes after a real edit; a separate connection avoids event/response mixing.
subscriber = Client()
subscriber.call('subscribe', pkg='serve_fixture')
source.write_text(fixed)
event = json.loads(subscriber.file.readline())
assert event['event'] == 'diagnostics' and event['pkg'] == 'serve_fixture' and not event['diagnostics'], event
print('watch warm check:', event['summary'], flush=True)
subscriber.close()
one, rows = cli('check', '--no-serve', '--json', 'serve_fixture')
auto, auto_rows = cli('check', '--json', 'serve_fixture')
assert one.returncode == auto.returncode == 0 and stable(rows) == stable(auto_rows)
for method,args in [('schema',['Counter']),('hover',[str(source),'16','23']),('definition',[str(source),'18','19'])]:
p,_ = cli(method,'serve_fixture',*args)
assert p.returncode == 0, (method,p.stderr)
bad = Client('0'*64)
bad.file.write(b'{"id":7,"token":"bad","method":"status","params":{}}\n'); bad.file.flush()
assert json.loads(bad.file.readline())['error']['code'] == -32001
assert bad.file.readline() == b''
bad.close()
malformed = Client()
malformed.file.write(b'{\n'); malformed.file.flush()
assert json.loads(malformed.file.readline())['error']['code'] == -32700
malformed.close()
client.call('shutdown'); client.close()
assert child.wait(timeout=15) == 0
assert not endpoint.exists() and not (endpoint.parent/'lock').exists()
# Explicit startup and automatic connection use the same endpoint and log.
started, rows = cli('check', '--serve', '--json', 'serve_fixture')
assert started.returncode == 0 and 'starting the check server' in started.stderr, started
c = Client(); c.call('shutdown'); c.close()
deadline = time.monotonic()+15
while endpoint.exists() and time.monotonic()<deadline: time.sleep(.02)
assert not endpoint.exists()
# SIGTERM cleanup and idle cleanup exercise the exact same lease guard.
for args, terminate in [([],True),(['--idle-min','0.002'],False)]:
child = subprocess.Popen([str(relax),'serve','--workspace',str(fixture),*args],env=env,stdout=log,stderr=log)
endpoint = await_endpoint()
if terminate: child.send_signal(signal.SIGTERM)
assert child.wait(timeout=15) == 0
assert not endpoint.exists() and not (endpoint.parent/'lock').exists()
print('serve: PASS; artifacts', work)
finally:
if endpoint is not None and endpoint.exists():
try:
cleanup = Client(); cleanup.call('shutdown'); cleanup.close()
except (OSError, ValueError, AssertionError): pass
if child.poll() is None:
child.terminate()
try: child.wait(timeout=15)
except subprocess.TimeoutExpired: child.kill(); child.wait()
log.close()