288 lines
14 KiB
Python
288 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
# Oracle for taicpp/tests/mplex_vt_test.cpp: loads ../tai.py, generates seeded cases for
|
|
# TuiVtScreen and MplexKeys and writes cases with the Python results next to them.
|
|
# usage: python3 -I mplex_vt_oracle.py <tai.py> <out cases file> [--show N]
|
|
import collections
|
|
import hashlib
|
|
import os
|
|
import importlib.util
|
|
import random
|
|
import sys
|
|
|
|
tai_path, out_path = sys.argv[1], sys.argv[2]
|
|
spec = importlib.util.spec_from_file_location("tai_mod", tai_path)
|
|
tai = importlib.util.module_from_spec(spec)
|
|
sys.modules["tai_mod"] = tai
|
|
spec.loader.exec_module(tai)
|
|
|
|
|
|
def hx(data):
|
|
if isinstance(data, str):
|
|
data = data.encode("utf-8", "surrogatepass")
|
|
return data.hex() if data else "-"
|
|
|
|
|
|
def cell_text(cell):
|
|
if cell is None:
|
|
return "-"
|
|
char, fg, bg, attr = cell
|
|
return "%x:%s:%s:%d" % (ord(char), fg or "_", bg or "_", attr)
|
|
|
|
|
|
def row_text(row):
|
|
return " ".join(cell_text(c) for c in row) if row else "."
|
|
|
|
|
|
def dump(screen):
|
|
out = []
|
|
out.append("size %d %d" % (screen.rows, screen.cols))
|
|
out.append("cur %d %d %d" % (screen.cx, screen.cy, 1 if screen.cursor_visible else 0))
|
|
out.append("sgr %s %s %d" % (screen.fg or "_", screen.bg or "_", screen.attr))
|
|
out.append("region %d %d %d %d %d" % (screen.region_top, screen.region_bottom, 1 if screen.lr_mode else 0, screen.margin_left, screen.margin_right))
|
|
out.append("modes %d %d %d" % (screen.app_cursor, screen.bracketed_paste, screen.alt_active))
|
|
out.append("title " + hx(screen.title))
|
|
out.append("replies " + hx(bytes(screen.replies)))
|
|
if screen.history is None:
|
|
out.append("nohist")
|
|
else:
|
|
out.append("hist %d %d %d" % (screen.history_pushed, len(screen.history), screen.history.maxlen))
|
|
for row in screen.history:
|
|
out.append("h " + row_text(row))
|
|
for row in screen.grid:
|
|
out.append("g " + row_text(row))
|
|
out.append("lines " + "|".join(hx(t) for t in screen.text_lines()))
|
|
out.append("text " + hx(screen.text()))
|
|
out.append("text3 " + hx(screen.text(3)))
|
|
return "\n".join(out)
|
|
|
|
|
|
def run_vt(rows, cols, hist, ops):
|
|
screen = tai.TuiVtScreen(rows, cols)
|
|
if hist >= 0:
|
|
screen.history = collections.deque(maxlen=hist)
|
|
dumps = []
|
|
for kind, arg in ops:
|
|
exc = ""
|
|
try:
|
|
if kind == "f":
|
|
screen.feed(arg)
|
|
elif kind == "r":
|
|
screen.resize(arg[0], arg[1])
|
|
elif kind == "t":
|
|
screen.take_replies()
|
|
except ValueError as err:
|
|
exc = "\nexc ValueError"
|
|
dumps.append(dump(screen) + exc)
|
|
return dumps
|
|
|
|
|
|
rnd = random.Random(int(os.environ.get('MPLEX_SEED', '20260101')))
|
|
|
|
CSI_FINALS = list("ABCDEFGHJKLMPSTXZ@bdfhlmnpqrsuc")
|
|
PARAMS = ["", "0", "1", "2", "3", "5", "7", "9", "10", "25", "255", "300", "99999", "1;1", "2;3", "5;5", "1;80", "3;1;4", "38;5;12", "48;5;200", "38;2;1;2;3", "48;2;250;251;252", "1;31;44", "0;1", "4;7", "?25", "?1", "?2004", "?69", "?1049", "?47", "?1047", "?25;1", ">", ">0", "0", "$", "?25$", "?69$", "?1049$", "1;3", "2;20", "20;2", "1:2", "", "!", "\"", "= 1", " 5", "5 ", "1 2", "99999999999999999999999", "38;5;300", "38;2;1;2", "48;5", "0;0", "1;0"]
|
|
ESC_SIMPLE = ["7", "8", "M", "D", "E", "c", "(0", "(B", ")0", "#8", "%G", "=", ">"]
|
|
TEXT = ["hello", "world ", "x", "abc def", "$ ls\r\n", "\r\n", "\n", "\r", "\t", "\b", "\x07", "\x0e", "\x0f", "\x00", "\x7f", "\x0b", "\x0c",
|
|
"é", "€", "\U0001f600", "ä¸", "Ì€", "line one\r\nline two\r\n", "q" * 30, "z" * 100, "lqqqk", "xxx", "\x9b", "\x9d", "\x90", "\x9e", "\x9f"]
|
|
PART_UTF8 = [b"\xc3", b"\xe2\x82", b"\xf0\x9f\x98", b"\xc3\xa9", b"\xff", b"\x80", b"\xc0\xaf", b"\xe2\x28\xa1", b"\xf0\x28\x8c\xbc", b"\xed\xa0\x80", b"\xf4\x90\x80\x80", b"\xe0\x80\x80", b"\xf0\x80", b"\xc2"]
|
|
|
|
|
|
def rand_token():
|
|
k = rnd.random()
|
|
if k < 0.28:
|
|
return rnd.choice(TEXT).encode("utf-8")
|
|
if k < 0.62:
|
|
return b"\x1b[" + rnd.choice(PARAMS).encode() + rnd.choice(CSI_FINALS).encode()
|
|
if k < 0.70:
|
|
return b"\x1b" + rnd.choice(ESC_SIMPLE).encode()
|
|
if k < 0.76:
|
|
term = rnd.choice([b"\x07", b"\x1b\\", b""])
|
|
code = rnd.choice([b"0", b"2", b"1", b"52", b""])
|
|
return b"\x1b]" + code + b";" + rnd.choice([b"title", b"t\xc3\xa9", b"x" * 300, b""]) + term
|
|
if k < 0.80:
|
|
return rnd.choice([b"\x1bP", b"\x1bX", b"\x1b^", b"\x1b_"]) + b"data" + rnd.choice([b"\x07", b"\x1b\\"])
|
|
if k < 0.86:
|
|
return rnd.choice(PART_UTF8)
|
|
if k < 0.90:
|
|
return b"\xc2\x9b" + rnd.choice(PARAMS).encode() + rnd.choice(CSI_FINALS).encode()
|
|
if k < 0.94:
|
|
return b"\x1b[" + rnd.choice(PARAMS).encode()
|
|
return bytes(rnd.randrange(256) for _ in range(rnd.randrange(1, 6)))
|
|
|
|
|
|
def rand_stream():
|
|
return b"".join(rand_token() for _ in range(rnd.randrange(1, 14)))
|
|
|
|
|
|
def chunks(data):
|
|
out = []
|
|
pos = 0
|
|
while pos < len(data):
|
|
n = rnd.choice([1, 2, 3, 5, 8, 40, 1000])
|
|
out.append(data[pos:pos + n])
|
|
pos += n
|
|
return out or [b""]
|
|
|
|
|
|
cases = []
|
|
|
|
FIXED = [
|
|
b"$ echo hi\r\nhi\r\n$ ",
|
|
b"\x1b[31mred\x1b[0m \x1b[1;4;7mbold\x1b[22;24;27m \x1b[38;5;200mx\x1b[48;2;1;2;3my\x1b[39;49mz",
|
|
b"\x1b[2J\x1b[H\x1b[5;10Hx\x1b[3A\x1b[2B\x1b[4C\x1b[3D\x1b[6G\x1b[2d",
|
|
b"\x1b[3;8r" + b"a\r\n" * 12 + b"\x1b[r",
|
|
b"\x1b[?1049hALT\x1b[2;3Hxx\x1b[?1049l back",
|
|
b"\x1b[?47hone\x1b[?47l",
|
|
b"abcdef\x1b[3D\x1b[2P\x1b[3@\x1b[2X\x1b[0P\x1b[0@",
|
|
b"\x1b[1;1H" + b"line\r\n" * 30,
|
|
b"\xe2\x82", b"\xe2\x82\xac", b"\xff\xfe", b"\xf0\x9f\x98\x80",
|
|
b"\x1b]0;my title\x07\x1b]2;other\x1b\\\x1b]1;no\x07",
|
|
b"\x1b[5n\x1b[6n\x1b[c\x1b[>c\x1b[0c\x1b[?25$p\x1b[?1$p\x1b[?2004$p\x1b[?69$p\x1b[?1049$p\x1b[4$p\x1b[?7$p",
|
|
b"\x1b[?69h\x1b[3;10s\x1b[1;3Hab\x1b[1;5r\r\n\r\n\r\n\r\n\r\n\x1b[?69l",
|
|
b"\x1b(0lqqqk\x1b(Bplain\x1b[3b",
|
|
b"x" * 85 + b"\r\n" + b"y" * 80 + b"z",
|
|
b"\x1b7abc\x1b8d\x1b[s\x1b[3;3Hx\x1b[u",
|
|
b"\x1bM\x1bM\x1bD\x1bE\x1bc\x1b[3L\x1b[2M\x1b[1S\x1b[2T",
|
|
b"\x1b[1;1H\x1b[0J\x1b[1J\x1b[2J\x1b[3J\x1b[0K\x1b[1K\x1b[2K",
|
|
b"\x1b[?25l\x1b[?25h\x1b[?1h\x1b[?1l\x1b[?2004h\x1b[?2004l",
|
|
]
|
|
|
|
for rows, cols in ((24, 80), (5, 12), (1, 1), (3, 40), (10, 7)):
|
|
for stream in FIXED:
|
|
for hist in (-1, 3):
|
|
cases.append((rows, cols, hist, [("f", stream)], "fixed"))
|
|
|
|
for stream in FIXED:
|
|
cases.append((6, 20, 5, [("f", stream), ("r", (9, 11)), ("f", stream), ("r", (3, 30)), ("f", stream), ("r", (1, 2)), ("t", None), ("f", stream)], "resize"))
|
|
|
|
for _ in range(int(os.environ.get('MPLEX_FUZZ', '2400'))):
|
|
rows = rnd.choice([1, 2, 3, 5, 8, 12])
|
|
cols = rnd.choice([1, 2, 4, 9, 16, 24])
|
|
hist = rnd.choice([-1, 0, 2, 7, 100])
|
|
ops = []
|
|
for part in chunks(rand_stream()):
|
|
ops.append(("f", part))
|
|
if rnd.random() < 0.08:
|
|
ops.append(("r", (rnd.randrange(1, 12), rnd.randrange(1, 26))))
|
|
if rnd.random() < 0.05:
|
|
ops.append(("t", None))
|
|
cases.append((rows, cols, hist, ops, "fuzz"))
|
|
|
|
# ---- MplexKeys ----------------------------------------------------------------------
|
|
|
|
K = tai.MplexKeys
|
|
KEY_SPECS = [" ", "", " ", "a", "A", "C-a", "C-A", "M-a", "C-M-a", "M-C-a", "S-Up", "S-a", "S-A", "C-S-a", "^a", "^A", "C-m", "C-i", "C-[", "C-M-m", "Space", "Enter", "Tab", "BTab", "BSpace", "Escape", "Esc", "Up", "Down", "Left", "Right", "Home", "End", "PPage", "PageUp", "PgUp", "NPage", "PageDown", "PgDn", "IC", "Insert", "DC", "Delete", "Return", "Backspace", "Backtab", "F1", "F5", "F12", "F13", "F0", "f1", "C-F5", "M-F1", "S-F2", "C-M-S-Up", "C-Up", "M-Up", "C-Space", "M-Space", "C-x", "C-\\", "C-]", "C-^", "C-_", "C-@", "C-?", "C-1", "X", "foo", "C-foo", "C-", "M-", "é", "C-é", "S-é", "S-1", "C-S-1", "^", "^^", "^ ", " a ", "C- ", "M-M-a", "C-C-a", "S-S-a", "C-M-S-a", "-", "C--", "M--", "S--", "C-.", "M-<", "M->", "M-{", "ctrl", "C-b", "C-z", "None", "none"]
|
|
for ch in "!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`{|}~":
|
|
KEY_SPECS += [ch, "C-" + ch, "M-" + ch, "S-" + ch]
|
|
for name in ("Up", "Down", "Left", "Right", "Home", "End", "PPage", "NPage", "IC", "DC", "Enter", "Tab", "BTab", "BSpace", "Escape", "Space") + tuple("F%d" % n for n in range(1, 13)):
|
|
for mods in ("", "C-", "M-", "S-", "C-M-", "C-S-", "M-S-", "C-M-S-"):
|
|
KEY_SPECS.append(mods + name)
|
|
|
|
key_cases = []
|
|
|
|
|
|
def kc(kind, *args):
|
|
key_cases.append((kind, args))
|
|
|
|
|
|
for s in KEY_SPECS:
|
|
kc("canon", s)
|
|
|
|
CANON = []
|
|
for s in KEY_SPECS:
|
|
try:
|
|
CANON.append(K.canonical(s))
|
|
except tai.MplexError:
|
|
pass
|
|
for n in sorted(set(CANON)):
|
|
kc("tobytes", n, False)
|
|
kc("tobytes", n, True)
|
|
for n in ("Foo", "C-", "", "X-Up", "Up-", "C-F13", "F13", "C-Foo"):
|
|
kc("tobytes", n, False)
|
|
|
|
for b in range(256):
|
|
kc("namechar", bytes([b]))
|
|
for raw in (b"\xc3\xa9", b"\xe2\x82\xac", b"\xf0\x9f\x98\x80", b"\xff\xfe", b"ab", b"\xc3"):
|
|
kc("namechar", raw)
|
|
|
|
SEQS = [b"\x1b[A", b"\x1b[B", b"\x1b[C", b"\x1b[D", b"\x1b[H", b"\x1b[F", b"\x1b[Z", b"\x1bOA", b"\x1bOB", b"\x1bOC", b"\x1bOD", b"\x1bOH", b"\x1bOF", b"\x1bOP", b"\x1bOQ", b"\x1bOR", b"\x1bOS", b"\x1bOZ",
|
|
b"\x1b[1~", b"\x1b[2~", b"\x1b[3~", b"\x1b[4~", b"\x1b[5~", b"\x1b[6~", b"\x1b[7~", b"\x1b[8~", b"\x1b[9~", b"\x1b[11~", b"\x1b[12~", b"\x1b[13~", b"\x1b[14~", b"\x1b[15~", b"\x1b[17~", b"\x1b[18~", b"\x1b[19~", b"\x1b[20~", b"\x1b[21~", b"\x1b[23~", b"\x1b[24~", b"\x1b[25~",
|
|
b"\x1b[1;2A", b"\x1b[1;3A", b"\x1b[1;5A", b"\x1b[1;6B", b"\x1b[1;4C", b"\x1b[1;7D", b"\x1b[1;8H", b"\x1b[1;9F", b"\x1b[1;16A", b"\x1b[1;1A", b"\x1b[1;0A", b"\x1b[1;xA", b"\x1b[5;5~", b"\x1b[3;3~", b"\x1b[15;2~", b"\x1b[24;8~", b"\x1b[99;2~", b"\x1b[1;2Q",
|
|
b"\x1b[97u", b"\x1b[97;5u", b"\x1b[97;2u", b"\x1b[65;2u", b"\x1b[49;2u", b"\x1b[13u", b"\x1b[13;5u", b"\x1b[9;2u", b"\x1b[27u", b"\x1b[127u", b"\x1b[57417u", b"\x1b[57419;5u", b"\x1b[57441u", b"\x1b[57441;1:3u", b"\x1b[97;1:3u", b"\x1b[97;1:2u", b"\x1b[1u", b"\x1b[26u", b"\x1b[0u", b"\x1b[0;;97u", b"\x1b[57400u", b"\x1b[57344u", b"\x1b[31u", b"\x1b[32u", b"\x1b[97;;228u", b"\x1b[?1u", b"\x1b[u", b"\x1b[97;3u", b"\x1b[97;8u", b"\x1b[0;1;65:66u", b"\x1b[57426;3u", b"\x1b[57443u", b"\x1b[57443;1:2u", b"\x1b[57443;1:3u",
|
|
b"\x1b[M !!", b"\x1b[M`!!", b"\x1b[Mabc", b"\x1b[M ", b"\x1b[<0;10;5M", b"\x1b[<0;10;5m", b"\x1b[<64;1;1M", b"\x1b[<65;1;1M", b"\x1b[<35;1;1M", b"\x1b[<2;3;4M", b"\x1b[<0;1M", b"\x1b[<x;1;1M", b"\x1b[<0;1;1", b"\x1b[<3;1;1m", b"\x1b[<66;1;1M", b"\x1b[<67;1;1M", b"\x1b[<68;1;1M", b"\x1b[<4;1;1M", b"\x1b[< 0 ; 1 ; 2 M",
|
|
b"\x1b[200~hello\x1b[201~", b"\x1b[200~", b"\x1b[200~abc", b"\x1b[201~", b"\x1b[200~\x1b[201~x",
|
|
b"\x1b[?1;2$y", b"\x1b[?25;0$y", b"\x1b[0n", b"\x1b[12;40R", b"\x1b[?62;c",
|
|
b"\x1b]0;t\x07", b"\x1bP1$r\x1b\\", b"\x1bX", b"\x1b^", b"\x1b_",
|
|
b"\x1b\x1b", b"\x1b\x1b[A", b"\x1ba", b"\x1bA", b"\x1b ", b"\x1b\r", b"\x1b\n", b"\x1b\x7f", b"\x1b\x01", b"\x1b\x00", b"\x1b\xc3\xa9", b"\x1b\xc3", b"\x1b\xe2\x82\xac", b"\x1b\xf0\x9f\x98\x80", b"\x1b\xff", b"\x1b\xc0", b"\x1b1", b"\x1b<", b"\x1b[", b"\x1bO", b"\x1b[1;", b"\x1b[1", b"\x1b",
|
|
b"a", b"A", b" ", b"\r", b"\n", b"\r\n", b"\t", b"\x7f", b"\x00", b"\x01", b"\x1a", b"\x1c", b"\x1d", b"\x1e", b"\x1f", b"\xc3\xa9", b"\xe2\x82\xac", b"\xf0\x9f\x98\x80", b"\xc3", b"\xe2\x82", b"\xf0\x9f\x98", b"\xff", b"\x80", b"\xc0\xaf", b"\xf5\x80\x80\x80", b"\xe0\x80\x80", b"\xed\xa0\x80"]
|
|
|
|
for s in SEQS:
|
|
for final in (False, True):
|
|
kc("split", s, final)
|
|
kc("appcur", s)
|
|
for t in (b"x", b"\x1b[A" + s):
|
|
kc("split", t + s, True)
|
|
kc("split", t + s, False)
|
|
|
|
for _ in range(int(os.environ.get('MPLEX_KEYFUZZ', '2500'))):
|
|
n = rnd.randrange(1, 6)
|
|
data = b"".join(rnd.choice(SEQS) if rnd.random() < 0.85 else bytes(rnd.randrange(256) for _ in range(rnd.randrange(1, 4))) for _ in range(n))
|
|
if rnd.random() < 0.4:
|
|
data = data[:rnd.randrange(1, len(data) + 1)]
|
|
kc("split", data, False)
|
|
kc("split", data, True)
|
|
|
|
|
|
def key_result(kind, args):
|
|
try:
|
|
if kind == "canon":
|
|
return "ok " + hx(K.canonical(args[0]))
|
|
if kind == "tobytes":
|
|
return "ok " + hx(K.to_bytes(args[0], args[1]))
|
|
if kind == "namechar":
|
|
return "ok " + hx(K.name_of_char(args[0]))
|
|
if kind == "appcur":
|
|
return "ok " + hx(K.app_cursor_form(args[0]))
|
|
if kind == "split":
|
|
tokens, rest = K.split(args[0], args[1])
|
|
return "ok " + ";".join(hx(n) + "/" + hx(r) for n, r in tokens) + " " + hx(rest)
|
|
except tai.MplexError as exc:
|
|
return "err " + hx(str(exc))
|
|
raise SystemExit("bad kind")
|
|
|
|
|
|
def sha(text):
|
|
return hashlib.sha1(text.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def op_text(op):
|
|
kind, arg = op
|
|
if kind == "f":
|
|
return "f:" + hx(arg)
|
|
if kind == "r":
|
|
return "r:%dx%d" % arg
|
|
return "t:"
|
|
|
|
|
|
if len(sys.argv) > 4 and sys.argv[3] == "--show":
|
|
index = int(sys.argv[4])
|
|
# regenerate identically: cases list is deterministic
|
|
rows, cols, hist, ops, _tag = cases[index]
|
|
for line in run_vt(rows, cols, hist, ops):
|
|
print(line)
|
|
print("-----")
|
|
sys.exit(0)
|
|
|
|
with open(out_path, "w") as handle:
|
|
for rows, cols, hist, ops, tag in cases:
|
|
digests = " ".join(sha(d) for d in run_vt(rows, cols, hist, ops))
|
|
handle.write("VT\t%d\t%d\t%d\t%s\t=\t%s\n" % (rows, cols, hist, "\t".join(op_text(o) for o in ops), digests))
|
|
for kind, args in key_cases:
|
|
shown = []
|
|
for a in args:
|
|
if isinstance(a, bool):
|
|
shown.append("1" if a else "0")
|
|
else:
|
|
shown.append(hx(a))
|
|
handle.write("KEY\t%s\t%s\t=\t%s\n" % (kind, "\t".join(shown), key_result(kind, args)))
|
|
print("cases: %d vt, %d key" % (len(cases), len(key_cases)))
|