563 lines
29 KiB
Python
563 lines
29 KiB
Python
#!/usr/bin/env python3
|
|
# Generates the expected values for taicpp/tests/mplex_core_test.cpp by running the real
|
|
# ../tai.py classes (MplexCommandParser, MplexArgs, MplexStyle, MplexOptions, MplexConfig,
|
|
# MplexFormat, MplexLayout). Run from taicpp/: python3 -I tests/mplex_core_gen.py
|
|
# Writes build/core_cases.txt and build/core_cfg/*.toml (nothing else).
|
|
import importlib.util
|
|
import os
|
|
import random
|
|
import sys
|
|
import time
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
ROOT = os.path.dirname(HERE)
|
|
os.environ["TZ"] = "UTC"
|
|
time.tzset()
|
|
os.environ["VISUAL"] = "nvi"
|
|
os.environ.pop("EDITOR", None)
|
|
|
|
spec = importlib.util.spec_from_file_location("tai_mod", os.path.join(ROOT, "..", "tai.py"))
|
|
tai = importlib.util.module_from_spec(spec)
|
|
sys.modules["tai_mod"] = tai
|
|
spec.loader.exec_module(tai)
|
|
|
|
OUT = os.path.join(ROOT, "build")
|
|
CFG = os.path.join(OUT, "core_cfg")
|
|
os.makedirs(CFG, exist_ok=True)
|
|
rng = random.Random(20260701)
|
|
lines = []
|
|
|
|
|
|
def esc(text):
|
|
out = []
|
|
for ch in text:
|
|
o = ord(ch)
|
|
if ch == "\\":
|
|
out.append("\\\\")
|
|
elif ch == "\n":
|
|
out.append("\\n")
|
|
elif ch == "\t":
|
|
out.append("\\t")
|
|
elif ch == "\r":
|
|
out.append("\\r")
|
|
elif o < 0x20 and ch not in "":
|
|
out.append("\\x%02x" % o)
|
|
else:
|
|
out.append(ch)
|
|
return "".join(out)
|
|
|
|
|
|
def emit(*fields):
|
|
lines.append("\t".join(esc(str(f)) for f in fields))
|
|
|
|
|
|
def enc_list(items):
|
|
return str(len(items)) + "".join("\x1f" + str(x) for x in items)
|
|
|
|
|
|
def outcome(func):
|
|
try:
|
|
return "OK:" + func()
|
|
except tai.MplexError as exc:
|
|
return "ERR:" + str(exc)
|
|
except Exception as exc: # noqa: BLE001
|
|
return "EXC:" + type(exc).__name__
|
|
|
|
|
|
# --------------------------------------------------------------------------- parser
|
|
|
|
P = tai.MplexCommandParser
|
|
parser_inputs = [
|
|
"", " ", "a", "a b", " a b ", "a;b", "a ; b", "a;;b;", ";", "a\nb", "a\\\nb", "a\\b", "a\\", "\\", "'a b'", "'a;b' c",
|
|
"'unterminated", '"unterminated', '"a b"', '"a\\nb"', '"a\\tb\\e\\r\\q"', '"a\\"b"', '"a\\', '"\\', "a'b c'd", 'a"b c"d',
|
|
"{ a b }", "{a;b}", "{ send-keys -X cancel }", "x {a b}", "{a {b} c}", "{a '}' c}", '{a "}" c}', "{a \\} c}", "{", "{a", "{{a}",
|
|
"a { b }", "a{b}", "{a}b", "{a} {b}", "{}", "{ }", "command-prompt -I \"#S\" { rename-session \"%%\" }",
|
|
"confirm-before -p \"kill-window #W? (y/n)\" kill-window", "set -g status-left '[#S] ' ; set -g mouse on",
|
|
"a\tb", "a\rb", "café über", "'café'", '"\\é"', "\\é", "a;b\\;c", "a \\; b", "x '' y", 'x "" y',
|
|
"''", '""', "a''b", "a\"\"b", "send-keys -l 'hello world' ; send-keys Enter", "if-shell 'true' { display ok } { display no }",
|
|
"{\n one\n two\n}", "{ a } ; { b }", "bind-key -T prefix '|' split-window -h", "\\\\", "\\'", "'\\'", "\"\\\\\"",
|
|
"{'}", "{\"}", "{a\\", "{'a\\'}", "\u0001\u0002", "a\x00b", "; ;",
|
|
]
|
|
alphabet = ["a", "b", " ", " ", "'", '"', "\\", "{", "}", ";", "\n", "$", "é", "#", "n", "t", "-", "x y", "{a}", "'q'", '"w"']
|
|
for _ in range(1500):
|
|
parser_inputs.append("".join(rng.choice(alphabet) for _ in range(rng.randint(1, 14))))
|
|
for text in parser_inputs:
|
|
def run(text=text):
|
|
return "\x1e".join("\x1f".join(words) if words else "" for words in P.parse(text)) + ("|%d" % len(P.parse(text)))
|
|
emit("P", text, outcome(run))
|
|
words_for_quote = ["", "a", "a b", "a-b_c.d/e:f=g%h@i+j,k", "a\"b", "a\\b", "a$b", "café", "éè", "-", "--x", "x y z", "'", "☃", "1", "a;b", "{}", "\n", "\t"]
|
|
for word in words_for_quote:
|
|
emit("Q", word, P.quote(word))
|
|
|
|
# --------------------------------------------------------------------------- args
|
|
|
|
A = tai.MplexArgs
|
|
|
|
|
|
def flags_dump(args):
|
|
parts = []
|
|
for key in sorted(args.flags):
|
|
vals = ["T" if v is True else "S" + v for v in args.flags[key]]
|
|
parts.append(key + ":" + enc_list(vals))
|
|
gets = []
|
|
ints = []
|
|
for key in sorted(args.flags):
|
|
gets.append(key + "=" + repr(args.get(key)))
|
|
gets.append(key + "*" + repr(args.all(key)))
|
|
try:
|
|
ints.append(key + "=" + repr(args.int(key, 7)))
|
|
except tai.MplexError as exc:
|
|
ints.append(key + "=ERR " + str(exc))
|
|
return "\x1e".join([enc_list(parts), enc_list(args.positional), enc_list(gets), enc_list(ints)])
|
|
|
|
|
|
arg_values = ["", "x", "12", "-3", " 4 ", "1_0", "abc", "-", "--", "-x", "café", "0", "+5", "٣", "9" * 30]
|
|
for name, (aliases, spec_text, low, high) in tai.MPLEX_COMMAND_SPECS.items():
|
|
letters = [c for c in spec_text if c != ":"]
|
|
takes = {spec_text[i - 1] for i in range(1, len(spec_text)) if spec_text[i] == ":"}
|
|
cases = []
|
|
cases.append([])
|
|
cases.append(["pos"])
|
|
cases.append(["pos1", "pos2"])
|
|
cases.append(["a", "b", "c", "d"])
|
|
cases.append(["--"])
|
|
cases.append(["--", "-x"])
|
|
cases.append(["-"])
|
|
cases.append(["-q"])
|
|
cases.append(["-é"])
|
|
cases.append(["-" + (letters[0] if letters else "z")])
|
|
for _ in range(40):
|
|
words = []
|
|
for _i in range(rng.randint(0, 6)):
|
|
roll = rng.random()
|
|
if roll < 0.45 and letters:
|
|
combo = "".join(rng.choice(letters) for _k in range(rng.randint(1, 3)))
|
|
words.append("-" + combo)
|
|
elif roll < 0.6 and takes:
|
|
letter = rng.choice(sorted(takes))
|
|
if rng.random() < 0.5:
|
|
words.append("-" + letter + rng.choice(arg_values))
|
|
else:
|
|
words.append("-" + letter)
|
|
words.append(rng.choice(arg_values))
|
|
elif roll < 0.7:
|
|
words.append("--")
|
|
else:
|
|
words.append(rng.choice(arg_values))
|
|
cases.append(words)
|
|
for words in cases:
|
|
def run(name=name, spec_text=spec_text, words=words, low=low, high=high):
|
|
return flags_dump(A.parse(name, spec_text, words, low, high))
|
|
emit("A", name, spec_text, low, -1 if high is None else high, enc_list(words), outcome(run))
|
|
|
|
# --------------------------------------------------------------------------- style
|
|
|
|
S = tai.MplexStyle
|
|
colors = ["", "default", "terminal", "red", "RED", " Blue ", "brightred", "brightwhite", "bright", "brightx", "colour0", "colour255",
|
|
"colour256", "color5", "colour", "colour 5", "colour005", "colour²", "colour٣", "#ff0000", "#FFAA00", "#12345", "#1234567",
|
|
"#gg0000", "# 1 2 3", "#+f+f+f", "#-f0000", "# f 0 0 ", "#0x0000", "#_f0000", "#f_0000", "#ff_ff_", "colour" + "9" * 30, "nonsense", "é", "BrightBlack"]
|
|
for text in colors:
|
|
for bg in (False, True):
|
|
emit("C", text, int(bg), outcome(lambda text=text, bg=bg: repr(S.color(text, bg))))
|
|
frags = [None, "31", "42", "93", "104", "38;5;12", "48;5;200", "38;2;1;2;3", "48;2;255;0;128"]
|
|
texts = ["", "default", "bg=green,fg=black", "bold", "noattr,bg=yellow,fg=black", "fg=red bg=blue", "fg=default", "nobold", "none",
|
|
"bold,dim,italics,underscore,blink,reverse,hidden,strikethrough", "bright", "nodim,noitalics", "align=left,list=on", "range=left", "fill=red",
|
|
"push-default", "pop-default", "norange", "nolist", "fg=bogus", "bg=#zzzzzz", "xyz", "no", "nobogus", " , ,", "FG=RED,BG=Blue,BOLD",
|
|
"fg=colour196,bg=colour21,underline", "fg=#112233,bg=#445566", "default,bold", "bold,default", "fg=red,default,bg=blue", "noreverse", "reverse,noreverse",
|
|
"underline,nounderline", "dim,nobold", "é"]
|
|
for text in texts:
|
|
for base in [(None, None, 0), ("31", "42", 5), ("38;5;12", None, 255)]:
|
|
for reset in (None, (None, "104", 2)):
|
|
def run(text=text, base=base, reset=reset):
|
|
r = S.parse(text, base, reset)
|
|
return "|".join(["-" if x is None else str(x) for x in r])
|
|
emit("S", text, enc_list(["-" if x is None else x for x in base[:2]]) + "\x1e" + str(base[2]),
|
|
"-" if reset is None else enc_list([reset[0] or "-", reset[1] or "-"]) + "\x1e" + str(reset[2]), outcome(run))
|
|
for fg in frags:
|
|
for bg in frags:
|
|
for attr in (0, 1, 2, 4, 8, 16, 32, 64, 128, 255, 37):
|
|
emit("G", fg or "-", bg or "-", attr, S.sgr(fg, bg, attr))
|
|
|
|
# --------------------------------------------------------------------------- options
|
|
|
|
O = tai.MplexOptions
|
|
|
|
|
|
def show_val(v):
|
|
if v is True:
|
|
return "bool:True"
|
|
if v is False:
|
|
return "bool:False"
|
|
if isinstance(v, int):
|
|
return "int:%d" % v
|
|
return "str:" + v
|
|
|
|
|
|
samples = {
|
|
"str": ["", "x", "hello world", "#{x}", "café"],
|
|
"bool": ["on", "off", "yes", "no", "true", "false", "1", "0", " ON ", "TRUE", "maybe", "", "2", "é"],
|
|
"key": ["C-a", "None", "none", "", " none ", "M-x", "F5", "BTab", "C-M-a", "S-Up", "^a", "Space", "bogus", "C-", "C-é", "Escape", "PageUp", " "],
|
|
"colour": ["red", "default", "colour5", "#ffffff", "bogus", "", "brightblue", "colour999"],
|
|
"style": ["bg=red", "default", "bold,fg=blue", "bogus", "", "#{?x,a,b}", "fg=zz", "noattr,bg=yellow,fg=black"],
|
|
"int": ["0", "1", "5", "-1", "10001", "x", "", " 7 ", "1_0", "999999999999", "3.5", "+4", "٤"],
|
|
"choice": ["vi", "emacs", "top", "bottom", "left", "centre", "center", "right", "12", "24", "single", "double", "heavy", "simple", "on", "external", "off", "VI", "", "x"],
|
|
}
|
|
names = [d for d in O.DEFS]
|
|
for name in names:
|
|
kind = O.DEFS[name][0]
|
|
base_kind = kind.split(":")[0]
|
|
pool = list(samples[base_kind])
|
|
if base_kind == "int":
|
|
_t, lo, hi = kind.split(":")
|
|
pool += [lo, hi, str(int(lo) - 1), str(int(hi) + 1)]
|
|
for raw in pool:
|
|
emit("O", name, raw, outcome(lambda name=name, raw=raw: show_val(O.check(name, raw))))
|
|
for raw in ["x"]:
|
|
emit("O", "no-such-option", raw, outcome(lambda: show_val(O.check("no-such-option", "x"))))
|
|
emit("O", "mouse", raw, outcome(lambda: show_val(O.check("mouse", "on"))))
|
|
|
|
emit("D", enc_list([n for n in O.DEFS]), enc_list([show_val(O.DEFS[n][1]) + "~" + O.DEFS[n][0] for n in O.DEFS]), O.WORD_SEPARATORS)
|
|
|
|
|
|
def dump_levels(levels):
|
|
return "\x1e".join(enc_list(["%s=%s" % (k, show_val(v)) for k, v in lvl.values.items()]) for lvl in levels)
|
|
|
|
|
|
for scenario in range(120):
|
|
g = O()
|
|
s = O(g)
|
|
w = O(s)
|
|
levels = [g, s, w]
|
|
emit("OBEGIN", scenario)
|
|
pool_names = rng.sample(names, 8) + ["status", "prefix", "mode-keys", "status-left", "history-limit", "bogus-name"]
|
|
for _ in range(40):
|
|
lvl = rng.randrange(3)
|
|
obj = levels[lvl]
|
|
name = rng.choice(pool_names)
|
|
op = rng.choice(["set", "set", "set", "append", "get", "get", "toggle", "unset", "is_set", "show"])
|
|
if name in O.DEFS:
|
|
kind = O.DEFS[name][0].split(":")[0]
|
|
raw = rng.choice(samples[kind])
|
|
else:
|
|
raw = "x"
|
|
if op in ("set", "append"):
|
|
res = outcome(lambda: show_val(obj.set(name, raw, append=(op == "append"))))
|
|
elif op == "get":
|
|
res = outcome(lambda: show_val(obj.get(name)))
|
|
elif op == "toggle":
|
|
res = outcome(lambda: (obj.toggle(name), "")[1])
|
|
elif op == "unset":
|
|
res = outcome(lambda: (obj.unset(name), "")[1])
|
|
elif op == "is_set":
|
|
res = "OK:" + str(obj.is_set(name))
|
|
else:
|
|
res = outcome(lambda: O.show(obj.get(name)))
|
|
emit("OOP", lvl, op, name, raw, res, dump_levels(levels))
|
|
|
|
# --------------------------------------------------------------------------- config
|
|
|
|
cfg_samples = [
|
|
"",
|
|
"[options]\nprefix = \"C-a\"\nprefix2 = \"F12\"\nbase-index = 1\nmode-keys = \"vi\"\nstatus-position = \"top\"\n",
|
|
"[options]\nstatus = false\nrenumber-windows = true\nhistory-limit = 10000\nstatus-left = \"[#S] \"\nstatus-style = \"bg=blue,fg=white\"\n",
|
|
"[options]\nbogus = 1\nstatus = 3.5\nprefix = [1]\nhistory-limit = 999999\nbase-index = \"x\"\nmode-keys = \"nano\"\nmouse = true\n",
|
|
"[options]\nstatus-left = 5\nprefix = 5\n",
|
|
"[options]\nwhen = 1979-05-27T07:32:00Z\nbase-index = 2024-01-01\n",
|
|
"[unknown]\nx = 1\n[options]\nstatus = true\n[other]\ny = 2\n",
|
|
"options = 3\nkeys = 4\n",
|
|
"options = \"a\"\n[keys]\nx = 1\n",
|
|
"[keys]\nclear = [\"prefix\"]\n[keys.prefix]\n\"|\" = \"split-window -h\"\n\"-\" = \"split-window -v\"\n\"C-b\" = false\nr = { command = \"source-file ~/.tai/mplex.toml\", note = \"reload\", repeat = false }\nUp = { command = \"select-pane -U\", repeat = true }\n",
|
|
"[keys.prefix]\n\"C-b\" = \"\"\nz = \"\"\nx = false\nbogus-key = \"split-window\"\n\"C-\" = \"split-window\"\nq = \"nosuchcommand\"\nw = \"split-window ; nosuchcommand\"\ne = \"\"\n",
|
|
"[keys.root]\n\"M-Left\" = \"previous-window\"\n\"M-Right\" = \"next-window\"\n[keys.copy-mode-vi]\ny = \"send-keys -X copy-selection-and-cancel\"\n[keys.mytable]\nA = \"display-message hi\"\n",
|
|
"[keys]\nclear = \"prefix\"\n[keys.prefix]\na = 1\nb = true\nc = [\"x\"]\nd = { command = 5 }\ne = { cmd = \"split-window\" }\nf = { command = \"split-window\", repeat = 1, note = 5 }\ng = { command = \" \" }\nh = { repeat = true }\n",
|
|
"[keys]\nclear = [\"prefix\", \"root\", 5, \"newtable\"]\n[keys.prefix]\nc = \"new-window\"\n",
|
|
"[keys]\nprefix = \"split-window\"\n[keys.root]\n",
|
|
"[keys]\nclear = [\"copy-mode\"]\n",
|
|
"[keys.prefix]\nSpace = { command = \"next-layout\", repeat = true, note = \"café\" }\n\"M-x\" = \"unsupported-cmd\"\nd = \"detach\"\nk = \"kill-ser\"\nl = \"display-menu\"\n",
|
|
"[keys.prefix]\nk = \"kill\"\n",
|
|
"[keys.prefix]\na = \"split-window { oops\"\nb = 'display-message \"unterminated'\n",
|
|
"this is not toml\n",
|
|
"[options\nstatus = true\n",
|
|
"[options]\nstatus = true\nstatus = false\n",
|
|
"[options]\nstatus = \"unterminated\n",
|
|
"[options]\nprefix = \"C-é\"\n",
|
|
"x = 1\ny = 2\n[options]\nstatus = true\n",
|
|
]
|
|
# many error producers to hit the ERRORS_MAX cap (50)
|
|
many = "[options]\n" + "".join("opt%d = 1\n" % i for i in range(70))
|
|
cfg_samples.append(many)
|
|
many_keys = "[keys.prefix]\n" + "".join("bogus%d = \"nosuchcommand\"\n" % i for i in range(60))
|
|
cfg_samples.append(many_keys)
|
|
# mutate good samples by deleting or inserting characters (random but deterministic)
|
|
good = cfg_samples[1] + cfg_samples[9] + cfg_samples[11]
|
|
for _ in range(60):
|
|
pieces = list(good)
|
|
for _k in range(rng.randint(1, 4)):
|
|
pos = rng.randrange(len(pieces))
|
|
if rng.random() < 0.5:
|
|
del pieces[pos]
|
|
else:
|
|
pieces.insert(pos, rng.choice(list("\"'=[]{}.,\n# -aZ0")))
|
|
cfg_samples.append("".join(pieces))
|
|
|
|
|
|
def cfg_dump(config):
|
|
opts = enc_list(["%s=%s" % (k, show_val(v)) for k, v in config.options.values.items()])
|
|
tabs = []
|
|
for tname, table in config.tables.items():
|
|
rows = ["%s\x1d%s\x1d%s\x1d%s" % (k, b.command, int(bool(b.repeat)), b.note) for k, b in table.items()]
|
|
tabs.append(tname + "\x1c" + enc_list(rows))
|
|
return "\x1e".join([opts, enc_list(tabs), enc_list(config.errors), str(config.skipped), config.path])
|
|
|
|
|
|
def write_cfg(index, data, mode="w"):
|
|
path = os.path.join("build", "core_cfg", "%03d.toml" % index)
|
|
with open(os.path.join(ROOT, path), "wb") as handle:
|
|
handle.write(data if isinstance(data, bytes) else data.encode("utf-8"))
|
|
return path
|
|
|
|
|
|
os.chdir(ROOT)
|
|
index = 0
|
|
for text in cfg_samples:
|
|
path = write_cfg(index, text)
|
|
index += 1
|
|
emit("K", path, cfg_dump(tai.MplexConfig.load(path)))
|
|
for data in (b"\xff\xfe[options]\n", b"[options]\nstatus = \"\xe2\x82\"\n", b"abc\xc3", b"\xc3\x28", b"\xe0\x80\x80", b"\xf5\x80\x80\x80", b"[options]\n\x80\n", b"\xed\xa0\x80"):
|
|
path = write_cfg(index, data)
|
|
index += 1
|
|
emit("K", path, cfg_dump(tai.MplexConfig.load(path)))
|
|
big = write_cfg(index, b"#" * (tai.MPLEX_CONFIG_MAX + 1))
|
|
index += 1
|
|
emit("K", big, cfg_dump(tai.MplexConfig.load(big)))
|
|
exact = write_cfg(index, b"#" * tai.MPLEX_CONFIG_MAX)
|
|
index += 1
|
|
emit("K", exact, cfg_dump(tai.MplexConfig.load(exact)))
|
|
emit("K", "build/core_cfg/missing.toml", cfg_dump(tai.MplexConfig.load("build/core_cfg/missing.toml")))
|
|
os.makedirs("build/core_cfg/adir.toml", exist_ok=True)
|
|
emit("K", "build/core_cfg/adir.toml", cfg_dump(tai.MplexConfig.load("build/core_cfg/adir.toml")))
|
|
emit("K", "build/core_cfg/missing/deeper.toml", cfg_dump(tai.MplexConfig.load("build/core_cfg/missing/deeper.toml")))
|
|
emit("K", "", cfg_dump(tai.MplexConfig.load("")))
|
|
emit("DEFAULTPATH", tai.MplexConfig.default_path("/home/x/.tai"), tai.MplexConfig.default_path("rel"))
|
|
fresh = tai.MplexConfig()
|
|
emit("K", "-", cfg_dump(fresh))
|
|
emit("LOOKUP", "".join("%s/%s=%s;" % (t, k, fresh.lookup(t, k) is not None) for t, k in (("prefix", "C-b"), ("prefix", "nope"), ("root", "C-b"), ("nonexistent", "x"), ("copy-mode", "Escape"))))
|
|
for t, k in (("prefix", "C-b"), ("prefix", "nope"), ("nonexistent", "x")):
|
|
emit("UNBIND", t, k, int(fresh.unbind(t, k)), cfg_dump(fresh)[:0])
|
|
|
|
# --------------------------------------------------------------------------- format
|
|
|
|
F = tai.MplexFormat
|
|
VARS = {
|
|
"session_name": "work", "session_id": "$3", "window_index": "2", "window_name": "vim edit", "window_flags": "*Z", "pane_index": "1",
|
|
"pane_title": "host:/home/user/project", "pane_id": "%7", "host": "box.example.com", "host_short": "box", "empty": "", "zero": "0",
|
|
"one": "1", "num_a": "7", "num_b": "2", "num_c": "-3.5", "big": "1e30", "tiny": "0.1", "path": "/usr/local/bin/tool.sh", "path2": "/",
|
|
"uni": "café über ☃ ok", "word": "hello", "sentence": "the quick brown fox", "stamp": "86400", "negstamp": "-1.5", "badstamp": "abc",
|
|
"hugestamp": "1e30", "fl": "2.5", "sp": "a b", "special": "a b;c'd\"e$f#g{h}i[j](k)*l?m<n>o&p|q~r\\s", "pane_in_mode": "0", "synchronize-panes": "0",
|
|
"inf": "inf", "nan": "nan", "ws": " 12 ", "a_b": "x", "cmd": "ls -l",
|
|
}
|
|
for k, v in VARS.items():
|
|
emit("V", k, v)
|
|
|
|
|
|
def resolver(name):
|
|
if name.startswith("("):
|
|
return "SH<" + name[1:] + ">"
|
|
return VARS.get(name)
|
|
|
|
|
|
fmt_cases = [
|
|
"", "plain", "#S #I #W #F #P #T #H #h #D", "##", "#", "a#", "##{x}", "#{session_name}", "#{missing}", "[#{session_name}] ", "#{",
|
|
"#{unterminated", "#(", "#(echo hi)", "#(echo #(inner) x)", "#(a", "#[fg=red]x#[default]", "#[bold", "#[a]#[b]", "x#,y", "x#}y", "#{?empty,yes,no}", "#{?one,yes,no}",
|
|
"#{?zero,yes,no}", "#{?missing,yes,no}", "#{?one,yes}", "#{?one}", "#{?empty,a,zero,b,one,c,d}", "#{?empty,a,zero,b,empty,c,d}", "#{?empty,a,zero,b}",
|
|
"#{?#{one},A,B}", "#{?pane_in_mode,fg=yellow,#{?synchronize-panes,fg=red,fg=green}}", "#{?window_flags,#{window_flags}, }",
|
|
"#{?one,a#,b,c}", "#{?one,#{?zero,x,y},z}", "#{==:#{num_a},7}", "#{==:a,a}", "#{==:a,b}", "#{!=:a,b}", "#{!=:a,a}", "#{<:1,2}", "#{<:10,9}", "#{<:b,a}", "#{>:b,a}", "#{<=:a,a}",
|
|
"#{>=:2,10}", "#{>=:abc,abd}", "#{&&:1,1}", "#{&&:1,0}", "#{||:0,0}", "#{||:0,1}", "#{&&:,1}", "#{||:#{one},#{zero}}", "#{e|+:7,2}", "#{e|-:7,2}", "#{e|*:7,2}", "#{e|/:7,2}", "#{e|%:7,2}",
|
|
"#{e|+:#{num_a},#{num_b}}", "#{e|/:1,0}", "#{e|%:1,0}", "#{e|*:1e30,1e30}", "#{e|+:0.1,0.2}", "#{e|/:1,3}", "#{e|*:big,1}", "#{e|+:x,1}", "#{e|==:1,1}", "#{e|!=:1,1}", "#{e|<:1,2}", "#{e|>:1,2}",
|
|
"#{e|<=:1,1}", "#{e|>=:1,2}", "#{e|?:1,2}", "#{e|%:-7,3}", "#{e|%:7,-3}", "#{e|%:-7.5,2}", "#{e|+:inf,1}", "#{e|*:inf,0}", "#{e|-:inf,inf}", "#{e|+:nan,1}", "#{e|*:1e300,1e300}", "#{e|*:-1,0}", "#{e|+:123456789012345678,1}",
|
|
"#{e|*:99999999999,99999999999}", "#{e|*:1e18,10}", "#{e|*:1e19,10}", "#{e|+:2e308,0}",
|
|
"#{m:*vim*,#{window_name}}", "#{m:vim,#{window_name}}", "#{m:V*,#{window_name}}", "#{m/i:V*,#{window_name}}", "#{m/r:^vim,#{window_name}}", "#{m/r:[,#{window_name}}", "#{m/ri:VIM,#{window_name}}",
|
|
"#{m/r:v.m,x}", "#{m:??m*,#{window_name}}", "#{m:[a-z]*,#{word}}", "#{m:[!a-z]*,#{word}}", "#{m:,}",
|
|
"#{=3:window_name}", "#{=-3:window_name}", "#{=0:window_name}", "#{=100:window_name}", "#{=-100:window_name}", "#{=/3/...:window_name}", "#{=/-3/...:window_name}", "#{=3:uni}", "#{=-4:uni}", "#{=21:pane_title}",
|
|
"#{=x:window_name}", "#{=:window_name}", "#{=/:window_name}", "#{=/5:window_name}", "#{=/5/a/b:sentence}", "#{=+3:sentence}", "#{= 3:sentence}",
|
|
"#{E:status}", "#{E:#{word}}", "#{T:#{word}}", "#{q:special}", "#{q:sp}", "#{n:word}", "#{n:uni}", "#{n:empty}", "#{b:path}", "#{d:path}", "#{b:path2}", "#{d:path2}", "#{d:word}",
|
|
"#{!:one}", "#{!:zero}", "#{!:empty}", "#{!:missing}", "#{!!:one}", "#{!!:word}", "#{!!:zero}", "#{l:literal text}", "#{l:#{word}}", "#{l:a:b}", "#{s/l/L/:word}", "#{s/l+/_/:sentence}", "#{s/(o)/[\\1]/:sentence}",
|
|
"#{s/[/x/:word}", "#{s/a/b:word}", "#{s//-/:word}", "#{s/ /_/:sentence}", "#{s/e/E/g:sentence}", "#{s/(?P<x>e)/<\\g<x>>/:word}", "#{s/a/\\q/:word}", "#{s/^/>/:word}", "#{s/$/</:word}", "#{s/\\w+/W/:sentence}",
|
|
"#{t:stamp}", "#{t:negstamp}", "#{t:badstamp}", "#{t:hugestamp}", "#{t:empty}", "#{t:inf}", "#{t:nan}", "#{t:fl}", "#{t:one}", "#{t:zero}",
|
|
"#{n;=3:word}", "#{=3;n:word}", "#{b;=3:path}", "#{=3;q:special}", "#{;:word}", "#{;;n:word}", "#{n;n:word}", "#{n;!:word}", "#{n;l:word}", "#{l;n:word}", "#{n;m:*,x}", "#{q;E:special}",
|
|
"#{x y:word}", "#{x#y:word}", "#{:word}", "#{a:b:c}", "#{unknown:word}", "#{n:#{word}}", "#{n:#{missing}}", "#{=3:#{sentence}}", "#{?one,#{=3:sentence},no}", "#{n:#(echo hi)}",
|
|
"#{session_name}:#{window_index}.#{pane_index}", "#{==:#{session_name},work}", "#{?#{==:#{session_name},work},mine,other}", "[#S:#I:#W#{?window_flags,#{window_flags}, }]",
|
|
"\"#{=21:pane_title}\" %H:%M %d-%b-%y", "#[fg=red]#{session_name}#[default] #{host}", "#{#{word}}", "#{#{#{word}}}", "#{x}#{y}", "#{?one,#{?one,#{?one,deep,d},d},d}",
|
|
"#{?one,#,,#,}", "#{?one,#},#}}", "#{?empty,#},#}}", "#, #}", "#{?one,a,b,c}", "#{?empty,a,b,c}", "#{==:#,,#,}", "#{?one,a#{b,c}", "#{?one,(a,b),c}", "#{?one,#(x,y),z}", "#{?one,#{x,y},z}",
|
|
"%H", "100%", "#{word}%", "café #{uni} ☃", "#é", "#{é}", "#{uni}#{uni}#{uni}", "#W#", "#WW", "#Z", "#{ }", "#{n: }", "#{?,a,b}", "#{?}", "#{?,}", "#{?,,}",
|
|
"#{e|+:#{num_a},#{num_c}}", "#{e|*:#{num_c},2}", "#{e|/:#{num_a},2}", "#{e|%:#{num_a},3}", "#{e|-:0,0}", "#{e|*:-0.0,1}", "#{e|/:6,3}", "#{e|/:7,2}", "#{e|+:1_000,1}", "#{e|+: 1 ,1}", "#{e|+:1e2,1}",
|
|
]
|
|
# deep nesting beyond DEPTH_MAX
|
|
nest = "x"
|
|
for _ in range(16):
|
|
nest = "#{?one," + nest + ",n}"
|
|
fmt_cases.append(nest)
|
|
nest2 = "#{word}"
|
|
for _ in range(16):
|
|
nest2 = "#{E:" + nest2 + "}"
|
|
fmt_cases.append(nest2)
|
|
frag = ["#{word}", "#{?one,a,b}", "#{n:sentence}", "#{=3:sentence}", "#S", "##", "#,", "#}", "text", " ", ",", "#{m:*,x}", "#{e|+:1,2}", "#{==:a,a}", "#(cmd)", "#[fg=red]", "{", "}", "(", ")", "#{?", ":", ";", "#{s/o/0/:word}", "%Y", "#{t:stamp}", "#{q:special}", "é"]
|
|
for _ in range(2500):
|
|
fmt_cases.append("".join(rng.choice(frag) for _ in range(rng.randint(1, 8))))
|
|
for text in fmt_cases:
|
|
emit("F", text, outcome(lambda text=text: F.expand(text, resolver)))
|
|
for text in ["", "no percent", "%Y-%m-%d %H:%M:%S", "%H:%M %d-%b-%y", "%a %A %b %B %j %y %%", "100%", "%", "%Q", "%E", "café %H", "%-d", "%s", "%Z %z"]:
|
|
for stamp in (0.0, 86400.5, 1700000000.0, 1e9):
|
|
emit("W", text, repr(stamp), outcome(lambda text=text, stamp=stamp: F.with_time(text, stamp)))
|
|
for text in ["", "#[a]", "x#[a]y#[b]z", "#[", "#[a", "#[a]]", "##[a]", "a#[b\nc]d", "#[]#[]", "#[a]#[b", "café#[x]é", "#[a] #[a]", "#[#[a]]", "#[[]]"]:
|
|
emit("X", text, F.strip_styles(text))
|
|
for value in (None, "", "0", "1", "x", " ", "00", "false"):
|
|
emit("TRUTHY", "-" if value is None else "S" + value, int(F.truthy(value)))
|
|
for opener, closer, text, start in (("{", "}", "#{a#{b}c}d", 1), ("(", ")", "#(a#(b)c)d", 1), ("{", "}", "#{a", 1), ("{", "}", "#{a}}", 1), ("{", "}", "x#{#}}", 2)):
|
|
emit("MC", opener, closer, text, start, F.match_close(text, start, opener, closer))
|
|
for text in ["a,b", "a#,b", "a,#{x,y},b", "a,#(x,y),b", "", ",", "a,", ",a", "a#{b,c}", "#,#,", "}a,b)", "a),b", "#x,y", "a#"]:
|
|
emit("SC", text, enc_list(F.split_commas(text)))
|
|
|
|
# --------------------------------------------------------------------------- layout
|
|
|
|
L = tai.MplexLayout
|
|
N = tai.MplexLayoutNode
|
|
|
|
|
|
class Pane:
|
|
def __init__(self, ident):
|
|
self.id = ident
|
|
self.x = self.y = 0
|
|
self.w = self.h = 1
|
|
self.last_active = 0.0
|
|
|
|
|
|
def tree_dump(node):
|
|
if node is None:
|
|
return "None"
|
|
if node.kind == "pane":
|
|
return "P%d(%d,%d,%d,%d)[%d,%d,%d,%d]" % (node.pane.id, node.x, node.y, node.w, node.h, node.pane.x, node.pane.y, node.pane.w, node.pane.h)
|
|
return "%s%s(%d,%d,%d,%d){%s}" % (node.kind, "r" if node.reverse else "", node.x, node.y, node.w, node.h, ",".join(tree_dump(c) for c in node.children))
|
|
|
|
|
|
def sync(root):
|
|
if root is None:
|
|
return
|
|
for leaf in root.leaves():
|
|
leaf.pane.x, leaf.pane.y, leaf.pane.w, leaf.pane.h = leaf.x, leaf.y, leaf.w, leaf.h
|
|
|
|
|
|
def check_links(node):
|
|
# parent pointers must match structure
|
|
for child in node.children:
|
|
assert child.parent is node
|
|
|
|
|
|
names_l = list(tai.MPLEX_LAYOUT_NAMES) + ["bogus-layout"]
|
|
for scenario in range(160):
|
|
emit("LBEGIN", scenario)
|
|
panes = [Pane(0)]
|
|
counter = 1
|
|
root = N("pane", pane=panes[0], w=80, h=24)
|
|
W, H = 80, 24
|
|
L.assign(root, 0, 0, W, H)
|
|
sync(root)
|
|
emit("LOP", "init", "", tree_dump(root))
|
|
for _ in range(rng.randint(15, 45)):
|
|
leaves = root.leaves()
|
|
roll = rng.random()
|
|
if roll < 0.28:
|
|
idx = rng.randrange(len(leaves))
|
|
kind = rng.choice(["lr", "tb"])
|
|
before = rng.random() < 0.5
|
|
size = rng.choice([None, None, 1, 2, 3, 5, 10, 30, 100, 0, -4])
|
|
arg = "%d %s %d %s" % (idx, kind, int(before), "N" if size is None else size)
|
|
newp = Pane(counter)
|
|
counter += 1
|
|
def run():
|
|
global root
|
|
new_root, fresh = L.split(root, leaves[idx], kind, before, newp, size)
|
|
root = new_root
|
|
panes.append(newp)
|
|
return tree_dump(root)
|
|
res = outcome(run)
|
|
emit("LOP", "split", arg, res)
|
|
if res.startswith("ERR"):
|
|
continue
|
|
L.assign(root, 0, 0, W, H)
|
|
sync(root)
|
|
emit("LOP", "assign", "%d %d" % (W, H), tree_dump(root))
|
|
elif roll < 0.42 and len(leaves) > 1:
|
|
idx = rng.randrange(len(leaves))
|
|
victim = leaves[idx]
|
|
root = L.remove(root, victim)
|
|
panes = [p for p in panes if p is not victim.pane] + []
|
|
emit("LOP", "remove", str(idx), tree_dump(root))
|
|
L.assign(root, 0, 0, W, H)
|
|
sync(root)
|
|
emit("LOP", "assign", "%d %d" % (W, H), tree_dump(root))
|
|
elif roll < 0.6:
|
|
idx = rng.randrange(len(leaves))
|
|
kind = rng.choice(["lr", "tb"])
|
|
delta = rng.randint(-12, 12)
|
|
prefer = rng.random() < 0.5
|
|
res = outcome(lambda: (L.resize_by(leaves[idx], kind, delta, prefer), tree_dump(root))[1])
|
|
emit("LOP", "resize_by", "%d %s %d %d" % (idx, kind, delta, int(prefer)), res)
|
|
L.assign(root, 0, 0, W, H)
|
|
sync(root)
|
|
emit("LOP", "assign", "%d %d" % (W, H), tree_dump(root))
|
|
elif roll < 0.72:
|
|
idx = rng.randrange(len(leaves))
|
|
kind = rng.choice(["lr", "tb"])
|
|
size = rng.randint(1, 60)
|
|
res = outcome(lambda: (L.resize_to(leaves[idx], kind, size), tree_dump(root))[1])
|
|
emit("LOP", "resize_to", "%d %s %d" % (idx, kind, size), res)
|
|
L.assign(root, 0, 0, W, H)
|
|
sync(root)
|
|
emit("LOP", "assign", "%d %d" % (W, H), tree_dump(root))
|
|
elif roll < 0.82:
|
|
W = rng.choice([1, 2, 3, 5, 8, 20, 40, 80, 120, 200, 233])
|
|
H = rng.choice([1, 2, 3, 4, 6, 12, 24, 40, 50, 77])
|
|
L.assign(root, 0, 0, W, H)
|
|
sync(root)
|
|
emit("LOP", "assign", "%d %d" % (W, H), tree_dump(root))
|
|
elif roll < 0.92:
|
|
for p in panes:
|
|
p.last_active = float(rng.randint(0, 5))
|
|
emit("LOP", "active", enc_list([int(p.last_active) for p in panes]), "")
|
|
for p in panes:
|
|
for d in "LRUD":
|
|
got = L.neighbour([leaf.pane for leaf in root.leaves()], p, d, W, H)
|
|
emit("LOP", "neighbour", "%d %s %d %d" % (p.id, d, W, H), "-" if got is None else got.pane.id if hasattr(got, "pane") else got.id)
|
|
else:
|
|
name = rng.choice(names_l)
|
|
count = rng.randint(1, 9)
|
|
mw = rng.choice([1, 5, 20, 80, 200])
|
|
mh = rng.choice([1, 5, 12, 24, 100])
|
|
pset = [Pane(counter + i) for i in range(count)]
|
|
counter += count
|
|
panes = pset
|
|
root = L.preset(name, pset, W, H, mw, mh)
|
|
emit("LOP", "preset", "%s %d %d %d %d %d" % (name, count, W, H, mw, mh), tree_dump(root))
|
|
L.assign(root, 0, 0, W, H)
|
|
sync(root)
|
|
emit("LOP", "assign", "%d %d" % (W, H), tree_dump(root))
|
|
|
|
for name in names_l:
|
|
for count in range(1, 10):
|
|
for (W, H) in [(1, 1), (2, 2), (3, 7), (10, 5), (20, 10), (80, 24), (80, 23), (81, 25), (120, 40), (200, 50), (233, 61)]:
|
|
for (mw, mh) in [(80, 24), (1, 1), (200, 100)]:
|
|
pset = [Pane(i) for i in range(count)]
|
|
root = L.preset(name, pset, W, H, mw, mh)
|
|
pre = tree_dump(root)
|
|
L.assign(root, 0, 0, W, H)
|
|
sync(root)
|
|
emit("LPRESET", "%s %d %d %d %d %d" % (name, count, W, H, mw, mh), pre, tree_dump(root))
|
|
for total in range(-2, 40):
|
|
for count in range(1, 9):
|
|
for first in (True, False):
|
|
emit("LSHARES", total, count, int(first), enc_list(L.shares(total, count, first)))
|
|
|
|
open(os.path.join(OUT, "core_cases.txt"), "w", encoding="utf-8", newline="\n").write("\n".join(lines) + "\n")
|
|
print("cases:", len(lines))
|