Files
tai/taicpp/tests/capmcp_diff.py
T

183 lines
8.8 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Differential test of the C++ wire layer (JSON, TOML, str(), float repr,
regex) against CPython. Builds nothing: pass the compiled probe.
python3 -I tests/capmcp_diff.py PROBE_BINARY [--seed N] [--fuzz N]
Exit 0 only when every case prints the same line in both programs."""
import json
import os
import random
import re
import subprocess
import sys
import tempfile
import tomllib
JSON_CASES = [
"", " ", "x", "{", "{\"a\"", "{\"a\":", "{\"a\":1", "{\"a\":1,", "{\"a\":1,}", "[1,]", "[1", "[", "[1 2]",
"{\"a\" 1}", "{a:1}", "{'a':1}", "\"abc", "\"a\\x\"", "\"a\\u12\"", "\"a\nb\"", "01", "-", "-x", "1.", "1e",
"1.5e3", "nul", "tru", "NaN", "-Infinity", "Infinity", "{\"a\":1} x", "[] []", "\"\\ud83d\\ude00\"",
"123456789012345678901234567890", " {\"a\": [1, 2.5, \"x\", null, true, false]} ", "{\"a\":1,\"a\":2}",
"\"\\u00e9\"", "[1,\n2,\n x]", "\ufeff{}", "{\"a\":1 \"b\":2}", "{,}", "[,1]", "{\"a\":}", "\"\\", "\"\\u",
"\"\\u12", "{\"a\":1,,}", "tru e", "-0", "-0.0", "1E5", "1e400", "-1e400", "\"é\"", "\"日本語\"", "[\"😀\"]",
"{\"b\":1,\"a\":{\"z\":[],\"y\":{}}}", "0.1", "1e-7", "1e22", "123456789012345680000.0", "5e-324", "1.7976931348623157e308",
"9223372036854775807", "9223372036854775808", "-9223372036854775808", "-9223372036854775809",
"\"\\/\\b\\f\\n\\r\\t\\\"\\\\\"", "\"\x7f\"", "\"\\u007f\"", "\"\\u2028\"", "{\"a\":\n\n [\n1\n,\n2] }",
]
TOML_CASES = [
"", "a = 1", "a = 1\nb = 2.5\nc = \"x\"\nd = true\ne = [1, 2, 3]\nf = { g = 1, h = \"i\" }",
"[t]\na = 1\n[t.u]\nb = 2", "[[s]]\nname = \"a\"\n[[s]]\nname = \"b\"",
"a = ", "a", "= 1", "a = 1 b = 2", "a = 1\na = 2", "[t]\n[t]", "a = [1, 2", "a = {x = 1", "a = \"abc",
"a = 'abc'\nb = '''multi\nline'''\nc = \"\"\"x\ny\"\"\"", "a = \"\"\"line \\\n continued\"\"\"",
"a = 0x1F\nb = 0o17\nc = 0b101\nd = 1_000\ne = +5\nf = -7", "a = 1e3\nb = 1.5e-3\nc = inf\nd = -inf\ne = nan",
"a = 01", "a = 1__0", "a = 0x", "a = .5", "a = 5.", "a = 1979-05-27", "a = 1979-05-27T07:32:00Z",
"a = 1979-05-27 07:32:00.5-08:00", "a = 07:32:00", "a = 1979-02-30", "a = 2024-02-29",
"a.b.c = 1\na.b.d = 2", "a.b = 1\n[a]\nc = 2", "[a]\nb = 1\n[a.b]", "[a.b]\nc = 1\n[a]\nd = 2",
"a = [[1], [2, 3], []]", "a = [\n 1, # one\n 2,\n]\n", "a = { b = 1, }", "a = [ ,]",
"# comment only\n", "a = 1 # trailing\n", "a = \"x\" y", "\"quoted key\" = 1\n'lit key' = 2",
"a = \"\\u00e9\\U0001F600\"", "a = \"\\x\"", "a = \"\\ud800\"", "a = \"tab\there\"", "a = \"nl\nx\"",
"[ a . b ]\nc = 1", "[a]\nb = 1\n[[a.c]]\nd = 1\n[[a.c]]\nd = 2", "x = {a.b = 1, a.c = 2}",
"x = {a = 1}\n[x.b]", "a = [1, \"two\", 3.0]", "[a]\n[a.b]\n[a]", "a = 1\n[a]", "[a]\nb = 1\n[a]\nc = 2",
"\r\na = 1\r\nb = 2\r\n", "a = 1\rb = 2", "a = \"\"\"\"x\"\"\"\"", "a = '''x'''''", "a=\"\"\"\\ \n x\"\"\"",
"a = tru", "a = True", "[]", "[a", "[[a]", "[[a]]\n[a]", "[a]\n[[a]]", "a = [\n", "key = 'unterminated",
"bad key = 1", "a = \"\x01\"", "# \x01", "a = 1_000.000_1", "a = 1e+3_0", "a = 0e0", "a = -0.0",
"a = 123456789012345678", "a = 9223372036854775807", "a = 9223372036854775808",
"[agent]\nname = \"tai\"\nprofiles = [\"coding\"]\n[probe]\ncommand = [\"tai.py\", \"--version\"]\nversion_pattern = \"tai (\\\\d+)\"",
]
REGEX_CASES = [
("^tai\\[.*\\]› $", "tai[main]› "), ("^tai\\[.*\\]› $", "tai[main]›"), ("^>\\s*$", "> "), ("^❯\\s*$", "❯"),
("(?P<tool>\\w+)\\((?P<args>.*)\\)", "run(ls -l)"), ("(?P<status>ok|failed)?x", "x"),
("github\\.com/([^/\\s?#]+)/([^/\\s?#]+?)(?:\\.git)?(?:[/?#]|$)", "https://github.com/o/r.git"),
("github\\.com/([^/\\s?#]+)/([^/\\s?#]+?)(?:\\.git)?(?:[/?#]|$)", "https://github.com/o/r/tree/x"),
("(npx|uvx)\\s+([^\\s\"'`]+)", "run: npx -y pkg-name now"), ("@(cap://[A-Za-z0-9_.-]+)", "hi @cap://a-1 and @cap://b"),
("\\$\\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\\}", "x ${FOO:-bar} y"),
("\\$\\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\\}", "x ${FOO} y"),
("resource_metadata=\"([^\"]+)\"", "Bearer resource_metadata=\"https://x/y\", realm=x"),
("\\bno\\b", "say no way"), ("\\byes\\b", "yes."), ("seal error:", "seal error: boom"),
("^seal error:", "x\nseal error:"), ("end$", "the end\n"), ("end$", "the end\nmore"), ("a{2}", "caab aab"),
("a{,2}b", "aab"), ("x{", "x{"), ("}", "a}"), ("[]a]+", "]]a"), ("[^]a]+", "bc]"), ("\\d+\\.\\d+", "v 1.44.2"),
("tai (\\d+\\.\\d+\\.\\d+)", "tai 1.44.2"), ("(a)|(b)", "b"), ("é+", "ééé"), ("\\w+", "日本語 x"),
("(?:ab)+", "ababab"), ("a(?=b)", "ab"), ("a(?!b)", "ac"), ("\\Aab", "ab"), ("ab\\Z", "ab"),
]
REPR_CASES = [
"{\"a\":\"it's\",\"b\":[1,null,true,1.5,\"x\\n\"]}", "[\"a\\\"b'c\"]", "[\"\\u00e9\\u2028\\u00a0\\u0007\"]",
"{}", "[]", "[1e22, 1e-7, 0.1, 100.0, 1e16, 123456.789]", "\"plain\"", "null", "[true,false]", "{\"k\":{\"n\":[{}]}}",
"\"日本語\"", "\"\\ud83d\\ude00\"", "[NaN, Infinity, -Infinity]", "\"\\\\\"",
]
FLOAT_CASES = ["0", "1", "0.1", "1e22", "1e-7", "100", "1e16", "1e15", "123456789.123456789", "5e-324", "1.7976931348623157e308",
"0.0001", "0.00001", "12345678901234567890", "-2.5", "3.14159", "1e300", "2.5e-5", "0.30000000000000004"]
def write_cases(path, cases):
with open(path, "wb") as handle:
handle.write(b"\0".join(c.encode("utf-8", "surrogatepass") for c in cases))
def py_json(i, text):
try:
return "%d OK %s" % (i, json.dumps(json.loads(text)))
except ValueError as exc:
return "%d ERR %s" % (i, exc)
def py_toml(i, text):
try:
return "%d OK %s" % (i, json.dumps(tomllib.loads(text), default=str))
except ValueError as exc:
return "%d ERR %s" % (i, exc)
def py_repr(i, text):
try:
return "%d OK %s" % (i, repr(json.loads(text)))
except ValueError as exc:
return "%d ERR %s" % (i, exc)
def py_float(i, text):
return "%d OK %s" % (i, repr(float(text)))
def py_regex(i, text):
pattern, subject = text.split("\x01", 1)
try:
found = re.search(pattern, subject)
except re.error as exc:
return "%d ERR %s" % (i, exc)
if not found:
return "%d NONE" % i
groups = [found.group(0)] + list(found.groups())
return "%d OK %d %d %s %s" % (i, found.start(), found.end(), json.dumps(groups), json.dumps(found.groupdict()))
def fuzz_json(rng, count):
out = []
atoms = ["null", "true", "false", "0", "-1", "12", "3.5", "-2.5e3", "\"s\"", "\"\\u00e9\"", "\"a b\"", "[]", "{}", "NaN", "\"\\n\""]
for _ in range(count):
depth = rng.randint(0, 3)
def build(d):
if d == 0:
return rng.choice(atoms)
if rng.random() < 0.5:
return "[" + ", ".join(build(d - 1) for _ in range(rng.randint(0, 3))) + "]"
return "{" + ", ".join("\"k%d\": %s" % (rng.randint(0, 5), build(d - 1)) for _ in range(rng.randint(0, 3))) + "}"
text = build(depth)
out.append(text)
if text:
cut = rng.randint(0, len(text))
out.append(text[:cut])
pos = rng.randint(0, len(text) - 1) if len(text) > 1 else 0
out.append(text[:pos] + rng.choice([",", "]", "}", "\"", "\\", " ", "x", ":"]) + text[pos + 1:])
return out
def run(probe, mode, cases, pyfn, label):
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "cases.bin")
write_cases(path, cases)
done = subprocess.run([probe, mode, path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120)
got = done.stdout.decode("utf-8", "replace").splitlines()
want = [pyfn(i, c) for i, c in enumerate(cases)]
bad = 0
for i, (g, w) in enumerate(zip(got, want)):
if g != w:
bad += 1
if bad <= 8:
print("MISMATCH %s case %d %r\n cpp: %s\n py : %s" % (label, i, cases[i][:120], g[:300], w[:300]))
if len(got) != len(want):
bad += 1
print("MISMATCH %s: %d lines from probe, %d cases (stderr: %s)" % (label, len(got), len(want), done.stderr.decode()[:300]))
print("%-8s %4d cases, %d mismatches" % (label, len(cases), bad))
return bad
def main():
args = sys.argv[1:]
if not args:
print(__doc__)
return 2
probe = args[0]
seed = int(args[args.index("--seed") + 1]) if "--seed" in args else 7
fuzz = int(args[args.index("--fuzz") + 1]) if "--fuzz" in args else 300
rng = random.Random(seed)
bad = 0
jcases = JSON_CASES + fuzz_json(rng, fuzz)
bad += run(probe, "json", jcases, py_json, "json")
bad += run(probe, "repr", REPR_CASES, py_repr, "repr")
bad += run(probe, "float", FLOAT_CASES, py_float, "float")
bad += run(probe, "toml", TOML_CASES, py_toml, "toml")
bad += run(probe, "regex", ["%s\x01%s" % c for c in REGEX_CASES], py_regex, "regex")
print("TOTAL mismatches: %d" % bad)
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())