ludic.ui: a rule's selector text is interned - {sel} was sel itself (a template of one string hole passes the string through) and lss_rule frees sel, so rules <id> named whatever reused the bytes; smoke.txt's tree order follows the document, and smoke.py runs the transcript

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 20:10:13 +03:00
parent e74656c372
commit a436cfbe35
4 changed files with 38 additions and 3 deletions

View file

@ -19,7 +19,7 @@ function lss_rule_add(f: UiFile, decls: UiRule, sel: string, err: []string, line
let r = new UiRule let r = new UiRule
r.keys = decls.keys r.keys = decls.keys
r.vals = decls.vals r.vals = decls.vals
r.text = `{sel}` # a copy: the selector's text is given back below r.text = intern(sel) # a copy: lss_rule frees sel (`{sel}` was sel itself, and dangled)
r.at = `{f.path}:{line}` r.at = `{f.path}:{line}`
r.sel = sel_parse(sel, err) r.sel = sel_parse(sel, err)
if r.sel != null { if r.sel != null {

View file

@ -10,7 +10,8 @@ ludic ui-preview [--font DIR] # the host on stdin/stdout; DIR holds the gam
``` ```
**The wire protocol is [protocol-v1.md](protocol-v1.md)**, frozen: every request, every reply and **The wire protocol is [protocol-v1.md](protocol-v1.md)**, frozen: every request, every reply and
every line format. `smoke.txt` is a scripted session with what each reply must contain. every line format. `smoke.txt` is a scripted session with what each reply must contain, and `smoke.py` runs it
(`python3 tools/ui-preview/smoke.py tools/ui-preview/smoke.txt bin/ludic-ui-preview`).
## What it does ## What it does

34
tools/ui-preview/smoke.py Normal file
View file

@ -0,0 +1,34 @@
#!/usr/bin/env python3
# smoke.py - runs smoke.txt against a ui-preview binary and checks each reply: `> ` lines are requests,
# `< ` a reply line must contain the text (in order), `<= ` the exact final line, `<! ` no line may
# python3 tools/ui-preview/smoke.py tools/ui-preview/smoke.txt bin/ludic-ui-preview
import subprocess, sys
reqs, exp = [], []
for line in open(sys.argv[1]):
line = line.rstrip("\n")
if line.startswith("> ") or line == ">": reqs.append(line[2:]); exp.append([])
elif line.startswith(("< ", "<= ", "<! ")) and reqs:
k, _, t = line.partition(" "); exp[-1].append((k, t))
p = subprocess.run([sys.argv[2]], input="\n".join(reqs) + "\n", capture_output=True, text=True, errors="replace", timeout=60)
out = p.stdout.splitlines()
replies, cur = [], []
for l in out:
cur.append(l)
if l == "ok" or l.startswith("ok ") or l.startswith("fail"):
replies.append(cur); cur = []
bad = 0
print(f"{len(reqs)} requests, {len(replies)} replies, exit {p.returncode}")
for i, r in enumerate(reqs):
rep = replies[i] if i < len(replies) else []
pos = 0
for k, t in exp[i]:
ok = True
if k == "<":
j = next((j for j in range(pos, len(rep)) if t in rep[j]), None)
ok = j is not None; pos = j if ok else pos
elif k == "<=": ok = bool(rep) and rep[-1] == t
elif k == "<!": ok = not any(t in x for x in rep)
if not ok:
bad += 1; print(f"FAIL request {i+1} `{r[:60]}`: {k} {t!r}\n reply: {rep[-6:]}")
print("smoke:", "OK" if bad == 0 else f"{bad} expectation(s) failed")
if p.stderr.strip(): print("stderr:", p.stderr.strip()[:500])

View file

@ -103,8 +103,8 @@
> tree > tree
< node 0 < node 0
< "root" < "root"
< "go"
< "Food" < "Food"
< "go"
<= ok <= ok
> box label > box label