The Principia Machina/TPC-PY/diff falcon.py
Appearance
"""diff_falcon 0.2.0: random differential run of tpc_falcon, Python against
PHP (0.1.1: SERVED entries in the source lists, SM10; 0.2.0: the
output-schema digest, schema_canon and schema_digest, C7, drawn after
the others so the earlier functions' streams are unchanged). Usage:
python3 diff_falcon.py <seed> [count]. Writes its cases to a
temporary file outside the tree (kept where TPC_DIFF_CASES names it)
(count cases per function: canon, draw, reconcile, stage, qmatch,
qcheck, source, hop, stop, schema_canon, schema_digest), runs
diff_falcon.php over it and compares every render byte for byte."""
import json
import os
import random
import subprocess
import sys
import tempfile
from tpc_falcon import (run_case, SETS, TYPES, falcon_canon, _netstring,
falcon_stop)
seed = int(sys.argv[1]) if len(sys.argv) > 1 else 1
count = int(sys.argv[2]) if len(sys.argv) > 2 else 20000
rnd = random.Random(seed)
T0 = 1789948800
DAY = 86400
TEXT = ["a", "b", "A", " ", " ", "\t", "\n", "\r", "e\u0301", "\u00e9",
"\u212b", "\u00c5", "\u1100\u1161", "\uac00", "\u00a0", "|", "\\",
"\u000b", "x", "\ufb01", "\u0301", "Opus", "\u00e7", "c\u0327"]
SYMS = ["m", "g", "s", "A", "K", "mol", "cd", "rad", "sr", "Hz", "N", "Pa",
"J", "W", "C", "V", "F", "Ohm", "S", "Wb", "T", "H", "lm", "lx", "Bq",
"Gy", "Sv", "kat", "min", "h", "d", "au", "deg", "arcmin", "arcsec",
"ha", "L", "t", "eV", "degC", "Da", "Np", "B", "dB", "ft", "x", "da"]
PREF = ["", "", "", "k", "m", "u", "da", "d", "c", "M", "G", "T", "h", "q",
"Q", "kd"]
SEGS = ["a", "B", ".", "..", "%2E", "%2e%2E", "%7e", "%2f", "", "c%41",
"x y", "\u00e9", "%zz", "@", ":", "[1]"]
def pick(xs):
return xs[rnd.randrange(len(xs))]
def r_text():
if rnd.random() < 0.04:
return pick([5, None, True, 1.5, ["a"]])
return "".join(pick(TEXT) for _ in range(rnd.randint(0, 6)))
def r_digits(lo=0, hi=3):
return "".join(pick("0012345679") for _ in range(rnd.randint(lo, hi)))
def r_decimal():
if rnd.random() < 0.04:
return pick([1.5, 2, None, "1,5", " 1", "\u0663", "1e", "e5"])
s = pick(["", "", "", "-", "+"]) + r_digits(0, 4)
if rnd.random() < 0.4:
s += "." + r_digits(0, 3)
if rnd.random() < 0.3:
s += pick("eE") + pick(["", "", "-", "+"]) + r_digits(0, 5)
return s
def r_unit():
if rnd.random() < 0.05:
return pick(["1", "", " m", "m ", "m/s", "kg.m"])
toks = []
for _ in range(rnd.randint(1, 3)):
t = pick(PREF) + pick(SYMS)
r = rnd.random()
if r < 0.3:
t += str(rnd.randint(-3, 3))
elif r < 0.35:
t += str(rnd.randint(-9999, 9999))
elif r < 0.37:
t += "0" + str(rnd.randint(0, 99))
toks.append(t)
return pick([" ", " ", " ", " "]).join(toks)
def r_unitvalue():
if rnd.random() < 0.04:
return pick([5, None, "5", "5 m"])
return r_decimal() if rnd.random() < 0.03 else (
str(r_decimal()) + " " + r_unit())
def r_date():
if rnd.random() < 0.04:
return pick([20260925, None, "2026-9-25", "2026-09-25T00:00Z"])
y, m, d = rnd.randint(0, 9999), rnd.randint(0, 13), rnd.randint(0, 32)
if rnd.random() < 0.5:
m, d = rnd.randint(1, 12), rnd.randint(1, 28)
if rnd.random() < 0.1:
m, d = 2, 29
return ("%04d-%02d-%02d" if rnd.random() < 0.6 else "%04d%02d%02d") % (
y, m, d)
def r_url():
if rnd.random() < 0.03:
return pick([None, 5, "", "//a/b", "http://a b/"])
s = pick(["http", "HTTP", "https", "HtTpS", "urn", "mailto", "1x", "a+b"])
s += ":"
if rnd.random() < 0.7:
s += "//"
if rnd.random() < 0.2:
s += pick(["user", "User:pw", "u%41", "a@b"]) + "@"
if rnd.random() < 0.15:
s += pick(["[FE80::1]", "[::a]", "[bad"])
else:
s += "".join(pick(["a", "B", "%41", "%c3%a9", "-", ".", "x",
"%zz", "\u00e9"])
for _ in range(rnd.randint(0, 4)))
if rnd.random() < 0.3:
s += pick([":80", ":", ":8x", ":0443"])
if rnd.random() < 0.8:
s += pick(["/", ""]) + "/".join(pick(SEGS)
for _ in range(rnd.randint(0, 4)))
if rnd.random() < 0.3:
s += "?" + pick(["b=1&a=2", "", "Q", "%7E"])
if rnd.random() < 0.2:
s += "#" + pick(["F", "", "%2f"])
return s
def r_value(spec):
t = spec["type"]
if t == "text":
return r_text()
if t == "decimal":
return r_decimal()
if t == "unitvalue":
return r_unitvalue()
if t == "date":
return r_date()
if t == "url":
return r_url()
if t == "enum":
return pick(spec["values"] + ["Open", 1, None])
return pick([True, False, True, False, "true", 1, None])
def r_spec(name="x"):
t = pick(TYPES)
s = {"name": name, "type": t, "class": "MECHANICAL"}
if t == "enum":
s["values"] = pick([["open", "closed"], ["OK", "STALE", "WRONG"]])
return s
def gen_canon():
spec = r_spec()
if rnd.random() < 0.02:
spec = pick([{"name": "x", "type": "number", "class": "MECHANICAL"},
{"name": "IV", "type": "text", "class": "MECHANICAL"},
{"name": "x", "type": "text", "class": "OTHER"}])
return {"value": r_value(spec), "spec": spec}
# Small value pools so that outputs often agree after canonical form.
POOL = {"text": ["x", " x", "x ", "e\u0301", "\u00e9", "y", "a\u00a0b",
"a b"],
"decimal": ["1.5", "1.50", "15e-1", "2", "2.0", "0.2"],
"unitvalue": ["9.81 m s-2", "9.810 s-2 m", "5 cm", "0.05 m"],
"date": ["2026-09-25", "20260925", "2026-09-26"],
"url": ["http://a/b", "HTTP://A/b", "http://a/./b", "http://a/B",
"http://a/%7e", "http://a/~"],
"boolean": [True, False]}
BAD = {"text": [5], "decimal": ["x", 1.5], "unitvalue": ["1 kdB"],
"date": ["2026-02-30"], "url": ["http://a b"], "boolean": ["true"],
"enum": ["bad"]}
KEYS = {"text": [["g", " g"], ["h", "h "], ["m"]],
"decimal": [["1", "1.0"], ["2", "2.00"]],
"date": [["20260925", "2026-09-25"], ["2026-09-24"]]}
def pool_value(spec):
if rnd.random() < 0.02:
return pick(BAD[spec["type"]])
if spec["type"] == "enum":
return pick(spec["values"])
return pick(POOL[spec["type"]])
def r_decl(s):
"""QUOTE AFFIX and QUOTE SCALE on a spec, mostly where allowed."""
if s.get("quote") and s["type"] in ("decimal", "unitvalue"):
if rnd.random() < 0.3:
s["affix"] = pick(["%", "$", "per cent", "pct", chr(0xb0) + "C"])
if rnd.random() < 0.3:
s["scale"] = pick(["million", "k", "bn", "M"])
if rnd.random() < 0.01:
s[pick(["affix", "scale"])] = pick(["%", "million", "ab", None, 5,
"lots"])
def gen_reconcile():
nf = rnd.randint(1, 3)
fields = []
for i in range(nf):
if rnd.random() < 0.3:
cols = [{"name": "k", "type": pick(["text", "decimal", "date"]),
"class": "MECHANICAL"}]
for j in range(rnd.randint(1, 2)):
s = r_spec("c%d" % j)
if rnd.random() < 0.4:
s["quote"] = True
r_decl(s)
if rnd.random() < 0.2:
s["class"] = "INSPECTION"
cols.append(s)
fields.append({"name": "r%d" % i, "type": "rows", "key": ["k"],
"class": "MECHANICAL", "columns": cols})
else:
s = r_spec("f%d" % i)
if rnd.random() < 0.4:
s["quote"] = True
r_decl(s)
if rnd.random() < 0.15:
s["class"] = "INSPECTION"
fields.append(s)
schema = {"class": pick(["FINDINGS", "PATCH", "SURVEY"]),
"narrow": rnd.random() < 0.3, "fields": fields}
if rnd.random() < 0.25:
schema["confidential"] = rnd.random() < 0.8
if rnd.random() < 0.005:
schema["confidential"] = pick(["yes", 1, None])
if rnd.random() < 0.02:
schema["class"] = "OTHER"
nout = pick([2] * 6 + [3] * 5 + [4]) if rnd.random() > 0.01 else 1
outputs = []
for i in range(nout):
acct = "a%d" % i if rnd.random() > 0.003 else "a0"
status = "OVERDUE" if rnd.random() < 0.02 else "RETURNED"
if status == "OVERDUE" or rnd.random() < 0.02:
outputs.append({"account": acct, "status": status,
"result": None})
continue
res = {}
for f in fields:
if f["type"] == "rows":
groups = KEYS[f["columns"][0]["type"]]
keys = [pick(g) for g in rnd.sample(
groups, rnd.randint(0, len(groups)))]
if keys and rnd.random() < 0.03:
keys.append(keys[0])
rows = []
for kv in keys:
row = {"k": kv}
for cspec in f["columns"][1:]:
row[cspec["name"]] = pool_value(cspec)
rows.append(row)
res[f["name"]] = rows
else:
res[f["name"]] = pool_value(f)
r = rnd.random()
if r < 0.01:
res["extra"] = 1
elif r < 0.02 and res:
del res[pick(sorted(res))]
elif r < 0.15:
res["IV"] = pick([["x"], [], ["a", "b"], ["x"], ["\u202e"]])
elif r < 0.16:
res["IV"] = pick(["x", [1]])
elif r < 0.17:
res = pick([[], "x", 5])
outputs.append({"account": acct, "status": status, "result": res})
ids = []
for f in fields:
if f["type"] != "rows":
ids.append(f["name"])
continue
kspec = f["columns"][0]
for kv in ["g", "h", "m", "1", "2", "2026-09-25", "2026-09-24"]:
try:
k = _netstring([falcon_canon(kv, kspec)]).decode("utf-8")
except ValueError:
continue
for cspec in f["columns"][1:]:
ids.append("%s[%s].%s" % (f["name"], k, cspec["name"]))
quotes = []
for i in range(nout):
q = {}
for iid in ids:
if rnd.random() < 0.5:
q[iid] = pick(["PASS", "FAIL", "UNKNOWN", "PASS"])
if rnd.random() < 0.01:
q["x"] = "MAYBE"
quotes.append(q)
if rnd.random() < 0.01:
quotes = quotes[:-1]
return {"outputs": outputs, "quotes": quotes, "schema": schema}
def gen_draw():
m = rnd.randint(2, 9) if rnd.random() < 0.9 else rnd.randint(0, 1)
cands = []
for i in range(m):
assigned = rnd.randint(0, 4)
cands.append({"account": "a%d" % i,
"sponsor": "S%d" % rnd.randint(1, 4),
"family": "V%d" % rnd.randint(1, 3),
"snapshot": "x%d" % rnd.randint(1, 4),
"tier": rnd.random() < 0.4,
"good_standing": rnd.random() < 0.95,
"sandbox": rnd.random() < 0.05,
"over_cap": rnd.random() < 0.05,
"window": rnd.randint(0, 3),
"ontime": rnd.randint(0, assigned),
"assigned": assigned})
accts = [c["account"] for c in cands]
nk = rnd.randint(0, min(2, m))
kept = rnd.sample(accts, nk)
prior = kept + [a for a in accts
if a not in kept and rnd.random() < 0.05]
n = rnd.randint(1, 3) if nk else pick([2, 2, 3])
wait = rnd.randint(0, 100)
drafted = rnd.randint(0, 1000)
now = drafted + pick([0, max(wait - 1, 0), wait, wait + 5])
r = "%032x" % rnd.getrandbits(128)
args = {"candidates": cands, "n": n,
"state": {"kept": kept, "prior": prior, "drafted": drafted,
"now": now, "falcon_wait": wait}, "r": r}
x = rnd.random()
if x < 0.01:
args["n"] = pick([0, 4, 1.0, True])
elif x < 0.02:
args["r"] = r.upper()
elif x < 0.03 and cands:
cands[0]["tier"] = 1
elif x < 0.04:
args["state"]["now"] = drafted - 1
return args
KINDS = ["LANDED"] * 6 + ["EVALUATED", "BLOCKED", "UNBLOCKED", "SUSPENDED",
"RESTORED", "VIOLATION"]
def gen_stage():
events = []
for _ in range(rnd.randint(0, 15)):
k = pick(KINDS)
t = T0 + rnd.randint(0, 40) * DAY + pick([0, 0, 60, 3600,
rnd.randint(0, DAY - 1)])
e = [k, t]
if k in ("LANDED", "SUSPENDED", "RESTORED", "VIOLATION"):
e.append("a%d" % rnd.randint(1, 3))
events.append(e)
as_of = T0 + rnd.randint(0, 200) * DAY + pick([0, 1, -1, 7200])
if rnd.random() < 0.5:
th = SETS[pick("ABC")]
else:
th = {"blocks": rnd.randint(1, 6), "dates": rnd.randint(1, 4),
"span_days": rnd.randint(0, 10),
"dormant_days": rnd.randint(1, 100)}
x = rnd.random()
if x < 0.01 and events:
events[0] = ["LANDED", events[0][1]]
elif x < 0.02:
events.append(["REGISTERED", T0])
elif x < 0.03:
th = {"blocks": 0, "dates": 1, "span_days": 0, "dormant_days": 1}
return {"events": events, "as_of": as_of, "thresholds": th}
# Quote match pieces for the TOKEN RULE: digits, signs (T1), separators
# and group spaces (T2), joins (T3), exponent forms (T4), affixes and
# per-cent words (T5), brackets and quotation marks (T7), scales (T8), Zs
# spaces, Cf characters, letters, whitespace and non-ASCII. NFC needs every
# character assigned by Unicode 13.0 except the two digits and letters of
# 15.0 and 15.1, which NFC leaves alone and the pinned table classifies.
QP = ["12", "123", "1", "2", "3", "5", "10", "234", "000", "72.4", "1,000",
".", ",", "-", "+", "<", "=", "~", "^", "/", ":", "'", "_", " ", " ",
"\t", "(", ")", "[", '"', "%", "$", "e", "E", " x ", "*", "cm", "a",
"x", "Opus", "opus", chr(0x2212), chr(0x2013), chr(0xb1), chr(0x2248),
chr(0x2264), chr(0x2265), chr(0x2019), chr(0xa0), chr(0x2009),
chr(0x202f), chr(0xd7), chr(0xb7), chr(0x207b), chr(0x2075), chr(0xb2),
chr(0x2030), chr(0x20ac), chr(0xb0), chr(0x2032), chr(0xe9),
"e" + chr(0x301), chr(0x301), chr(0x6771) + chr(0x4eac), chr(0x3001),
chr(0x663), chr(0x2007), chr(0x2002), chr(0x3000), chr(0x1680),
chr(0x200b), chr(0xad), chr(0x2060), chr(0xfeff), chr(0x200e),
chr(0x2044), chr(0x2215), chr(0x2236), chr(0x1e4f1), chr(0x2ebf0),
" million", "million", "Millions", " M", "k", " k", " K", " bn", "Bn",
" B", " m", "m", " thousand", " lakh", "s", " E", " e ", "**", " ** ",
" times 10^", "times", " per cent", "per", " cent", "percent", " pct",
"PCT", " / ", " - ", " = ", " models", "km", chr(0x2014), chr(0x2010),
chr(0x2e3a), chr(0x301c), chr(0xff0d), chr(0xff5e), chr(0xf7),
chr(0x29f8), chr(0x2053), "~", chr(0xbd), chr(0x2153), chr(0x2189),
" 1/2", chr(0xff05), chr(0xfe6a), chr(0x66a), chr(0x609), chr(0x60a),
" bps", "bp", " basis points", " basis", " point", " per mille",
"mille", "permille", " pp", " ppm", "PPB", " ppt", " percentage",
" mln", " BLN", " tn", "trn", " Mio", "mrd", " MM", " mm", " dozen",
" thousandths", "tenth", " quadrillion", " to "]
AFFIX = [None, None, "%", "$", chr(0xb0) + "C", chr(0x2030), chr(0x20ac),
"percent", "per cent", "pct", chr(0xff05), chr(0x66a), "bps",
"basis point", "basis points", "per mille", "pp", "ppm",
"percentage points"]
SCALE = [None, None, None, "million", "k", "K", "M", "bn", "m", "B",
"thousand", "lakh", "MM", "mln", "mio", "dozen", "thousandth",
"tenth"]
def r_hay():
return "".join(pick(QP) for _ in range(rnd.randint(0, 8)))
def r_needle(h):
if h and rnd.random() < 0.5:
i = rnd.randint(0, len(h) - 1)
return h[i:rnd.randint(i + 1, len(h))]
return "".join(pick(QP) for _ in range(rnd.randint(1, 3)))
def r_flags():
aff = pick(AFFIX)
sc = pick(SCALE)
acc = rnd.random() < 0.2
if rnd.random() < 0.01:
aff = pick(["ab", "", 5, "%%", "Percent", "points", "Basis Point"])
if rnd.random() < 0.01:
sc = pick(["lots", "", 5, "Million", "millions", "Mio", "mm",
"thousandths"])
if rnd.random() < 0.005:
acc = pick(["yes", 1, None])
return aff, sc, acc
def gen_qmatch():
parts = [pick(QP) for _ in range(rnd.randint(0, 8))]
h = "".join(parts)
if parts and rnd.random() < 0.5:
i = rnd.randint(0, len(parts) - 1)
v = "".join(parts[i:rnd.randint(i + 1, len(parts))])
else:
v = r_needle(h)
if rnd.random() < 0.01:
v = pick([12, None, ["12"]])
aff, sc, acc = r_flags()
return {"value": v, "quote": h, "affix": aff, "scale": sc,
"accounting": acc}
def gen_qcheck():
parts = [pick(QP) for _ in range(rnd.randint(0, 8))]
page = "".join(parts)
if parts and rnd.random() < 0.6:
i = rnd.randint(0, len(parts) - 1)
qparts = parts[i:rnd.randint(i + 1, len(parts))]
quote = "".join(qparts)
j = rnd.randint(0, len(qparts) - 1)
value = ("".join(qparts[j:rnd.randint(j + 1, len(qparts))])
if rnd.random() < 0.7 else r_needle(quote))
else:
quote = r_needle(page)
value = r_needle(quote)
if rnd.random() < 0.05:
page = None
if rnd.random() < 0.01:
value = 5
aff, sc, acc = r_flags()
return {"page": page, "quote": quote, "value": value, "affix": aff,
"scale": sc, "accounting": acc}
HOSTS = ["example.org", "Example.ORG", "docs.example.com", "api.example.net",
"evil.example.org", "example.org.", "127.0.0.1", "[::1]",
"exa mple.org", "%65xample.org", "example.org.evil.com",
"github.com", "GitHub.com"]
PATHS = ["", "/", "/org", "/org/", "/org2", "/models", "/modelsx", "/a", "/..",
"/.", "/%2E%2E", "/%2F", "/%7Euser", "/v1/list", "/bench.csv",
"/bench%2ecsv", "/p", "/..;", "/%252E%252E", "/;x", "/a;b", "/%5C",
"/%3B", "/%252F", "/%25252E", "/...", "/v1.2", "/repo", "/tree",
"/main", "/blob", "/issues", "/%C3%A9", "/%" + "25" * 6 + "41",
"/%" + "25" * 7 + "41", "/refs", "/REFS", "/%C0%AE", "/%ED%A0%80",
"/%E2%82", "/%F0%9F%98%80", "/%80", "/%25C0%25AE", "/%F4%90%80%80"]
def r_cite():
if rnd.random() < 0.02:
return pick([5, None, "", "https:/org/x"])
s = pick(["https", "https", "https", "HTTPS", "http", "ftp"]) + "://"
if rnd.random() < 0.05:
s += pick(["user", "u:p"]) + "@"
s += pick(HOSTS) + pick(["", "", "", ":443", ":8443", ":", ":444"])
s += "".join(pick(PATHS) for _ in range(rnd.randint(0, 3)))
if rnd.random() < 0.3:
s += "?" + pick(["format=csv", "format=json", "utm=x", "", "p=../x"])
if rnd.random() < 0.2:
s += "#" + pick(["top", ""])
return s
GH = ["CODE HOST", "github.com", "/*/*/tree/*/", "/*/*/blob/*/",
"/*/*/commit/*/"]
LISTS = [
[["EXACT", "https://data.example.org/bench.csv"],
["PREFIX", "https://example.org/org/"],
["PREFIX", "https://docs.example.com/models"],
["EXACT", "https://api.example.net:8443/v1/list?format=csv"]],
[["PREFIX", "https://example.org/a/"], ["PREFIX", "https://example.org/a/org/"]],
[["PREFIX", "https://example.org/org/"],
["EXACT", "https://example.org/org/p"]],
[["EXACT", "https://example.org"]],
[GH, ["PREFIX", "https://github.com/org/repo/tree/main/"],
["EXACT", "https://data.example.org/bench.csv"],
["PREFIX", "https://example.org/org/"]],
[GH, ["PREFIX", "https://github.com/org/repo/blob/0123abcd/"],
["EXACT", "https://github.com/org/repo/issues/1"]],
[["CODE HOST", "github.com", "/*/*/tree/*/"],
["PREFIX", "https://github.com/org/repo/tree/main/docs/"],
["PREFIX", "https://github.com/org/repo/tree/dev/"]], []]
DECL = ["https://example.org/org", "https://example.org/org/",
"https://Example.ORG/a/%7eb", "https://api.example.net:8443/v1/list",
"https://api.example.net:8443/v1/list?format=csv",
"https://docs.example.com/models/",
"https://github.com/org/repo/tree/main/",
"https://github.com/org/repo/commit/0123abcd/",
"https://docs.example.com/", "http://example.org/", "https://u@a.org/",
"https://10.0.0.1/", "https://example.org/p#f", "https://a.org:/",
"https://github.com/org/", "https://github.com/org/repo",
"https://example.org/a/..;/", "https://example.org/a/%2E%2E/b/",
"https://example.org/a;b", "https://Example.ORG/a/./b",
"https://github.com/org/repo/tree/refs/", "https://example.org/%C0%AE/",
"https://github.com/org/repo/blob/Refs/pull/"]
GOOD = DECL[:8]
SERVED = ["https://api.crossref.org/v1/works/10.1016/s0140-6736(97)11096-0",
"https://api.crossref.org/v1/works/10.1002/(SICI)1097-0266(199708)18:7%3C509::AID-SMJ882%3E3.0.CO%3B2-Z",
"https://export.arxiv.org/api/query?id_list=2601.12345v2",
"https://doi.org/ra/10.1000/x", "https://api.datacite.org/dois/10.5281/zenodo.1234567",
"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esummary.fcgi?db=pubmed&retmode=json&id=9500320",
"https://example.org/org/x", "https://data.example.org/bench.csv", "https://doi.org/",
"https://github.com/org/repo/raw/main/x", "http://doi.org/ra/x", "https://doi.org/ra/x#f",
"https://u@doi.org/ra/x", "https://api.crossref.org/v1/works/10.1000/a%2Fb"]
MARKS = [GH, ["CODE HOST", "github.com"], ["CODE HOST", "github.com", "/*/"],
["CODE HOST", "GitHub.com", "/*/*/tree/*/"],
["CODE HOST", "github.com", "/*/*/tree/*"],
["CODE HOST", "docs.example.com", "/*/"],
["CODE HOST", "github.com", "/*/*/*/"]]
def r_sources():
if rnd.random() < 0.5:
sources = [list(x) for x in pick(LISTS)]
else:
sources = [[pick(["EXACT", "PREFIX", "PREFIX"]), pick(GOOD)]
for _ in range(rnd.randint(0, 4))]
if rnd.random() < 0.3:
sources.insert(rnd.randint(0, len(sources)), pick(MARKS[:3]))
if rnd.random() < 0.5:
for _ in range(rnd.randint(1, 3)):
sources.insert(rnd.randint(0, len(sources)),
["SERVED", pick(SERVED[:10] if rnd.random() < 0.9 else SERVED)])
if rnd.random() < 0.05:
sources.append(["SERVED", pick(SERVED[:10]), "x"])
if rnd.random() < 0.05:
sources.append(pick([["LIKE", "https://a.org/"], "https://a.org/",
["EXACT", pick(DECL[9:])],
["PREFIX", pick(DECL[8:])], pick(MARKS)]))
return sources
def r_near(sources):
es = [x for x in sources if type(x) is list and len(x) == 2
and type(x[1]) is str]
if es and rnd.random() < 0.6:
cited = pick(es)[1] + pick(["", "", "", "", "/", "x", "/x", "/x/y", "/../x",
"?q=1", "#f", "/%2E%2E/x", "/%2Fx",
"/..;/x", "/%252E%252E/x", "/a;b",
"/%5Cx", "data.csv", "../issues/1",
"/%C0%AE%C0%AE/x", "/%ED%A0%80",
"/refs/pull/1/", "v3"])
if rnd.random() < 0.2:
cited = cited.replace("https", pick(["HTTPS", "http"]), 1)
if rnd.random() < 0.1:
cited = cited.replace("%3B", "%3b").replace("(", "%28").replace("-", "%2D")
return cited
return r_cite()
def gen_source():
sources = r_sources()
return {"cited": r_near(sources), "sources": sources}
def gen_hop():
sources = r_sources()
x = rnd.random()
valid = [i for i, e in enumerate(sources) if type(e) is list
and len(e) == 2 and e[0] in ("EXACT", "PREFIX")]
served = [i for i, e in enumerate(sources) if type(e) is list
and len(e) == 2 and e[0] == "SERVED"]
if x < 0.8 and valid:
matched = pick(valid)
elif x < 0.85 and served:
matched = pick(served)
elif x < 0.9 and sources:
matched = rnd.randrange(len(sources))
elif x < 0.97:
matched = rnd.randint(-1, len(sources) + 1)
else:
matched = pick(["0", None, 1.0, True])
return {"hop": r_near(sources), "matched": matched, "sources": sources}
def gen_stop():
lim = {"breaker": pick(["0", "0.1", "0.25", "0.3", "0.5", "1", "0.3333"]),
"window": pick([1, 2, 3, 5, 20])}
if rnd.random() < 0.01:
lim = pick([{"breaker": "0.30001", "window": 20},
{"breaker": 0.3, "window": 20},
{"breaker": "0.3", "window": 0}, {"breaker": "0.3"}])
counts = []
flying = []
n = 0
spoil = rnd.random() < 0.05
for _ in range(rnd.randint(0, 40)):
st = falcon_stop(counts, {"breaker": "0.3", "window": 20}
if type(lim.get("breaker")) is not str
or "window" not in lim or lim["window"] == 0
or len(lim["breaker"]) > 6 else lim)["status"]
x = rnd.random()
if spoil and x < 0.1:
counts.append(pick([["ISSUED", "f1"], ["FINISHED", "zz", 1, 0, 0],
["STOPPED"], ["RESUMED", 1]]))
break
if st == "STOPPED" and (not flying or x < 0.3):
counts.append(["RESUMED"])
elif st == "ISSUING" and (not flying or x < 0.5):
n += 1
counts.append(["ISSUED", "f%d" % n])
flying.append("f%d" % n)
elif flying:
fid = flying.pop(rnd.randrange(len(flying)))
items = rnd.randint(0, 10)
dec = rnd.randint(0, items)
unr = rnd.randint(0, items - dec)
if spoil and rnd.random() < 0.1:
counts.append(["FINISHED", fid, items, dec, items + 1])
break
counts.append(["FINISHED", fid, items, dec, unr])
else:
counts.append(["RESUMED"])
return {"counts": counts, "limits": lim}
# Strings for the schema digest: line ends, NFD and NFC, the escapes, the
# controls, DEL, U+2028, a character beyond the BMP (never a lone
# surrogate: TPC-PHP cannot decode one).
STXT = TEXT + ["\r\n", '"', "\\", "\u0000", "\u001f", "\u007f", "\u2028",
"\U0001f600", "\u00e9", "e\u0301"]
def r_stext():
return "".join(pick(STXT) for _ in range(rnd.randint(0, 4)))
def shuffled(d):
"""The same object with its members in a random order."""
ks = list(d)
rnd.shuffle(ks)
return {k: d[k] for k in ks}
def r_sspec(name, allow_rows):
if allow_rows and rnd.random() < 0.25:
cols = [{"name": "k", "type": pick(["text", "decimal", "date"]),
"class": "MECHANICAL"}]
for j in range(rnd.randint(1, 3)):
cols.append(r_sspec("c%d" % j, False))
key = ["k"] + (["c0"] if rnd.random() < 0.2 and cols[1]["class"] == "MECHANICAL"
and cols[1]["type"] != "rows" else [])
return shuffled({"name": name, "type": "rows", "key": key,
"class": "MECHANICAL", "columns": cols})
s = r_spec(name)
if s["type"] == "enum" and rnd.random() < 0.6:
vals = []
for _ in range(rnd.randint(1, 4)):
v = r_stext()
if v not in vals:
vals.append(v)
s["values"] = vals
x = rnd.random()
if x < 0.3:
s["quote"] = True
elif x < 0.55:
s["quote"] = False
r_decl(s)
if rnd.random() < 0.3:
s["class"] = "INSPECTION"
return shuffled(s)
def gen_schema():
fields = [r_sspec("f%d" % i, True) for i in range(rnd.randint(1, 3))]
schema = {"class": pick(["FINDINGS", "PATCH", "SURVEY"]),
"narrow": rnd.random() < 0.4, "fields": fields}
x = rnd.random()
if x < 0.3:
schema["confidential"] = True
elif x < 0.6:
schema["confidential"] = False
if rnd.random() < 0.06:
bad = rnd.randrange(10)
if bad == 0:
return {"schema": pick([None, ["class"], "FINDINGS", 5, True, {}])}
if bad == 1:
del schema["narrow"]
elif bad == 2:
schema["narrow"] = pick(["false", 0, None])
elif bad == 3:
schema["fields"] = pick([{}, [], [[]], [None]])
elif bad == 4:
fields[0]["name"] = pick(["IV", "a-b", "", 5, "1a"])
elif bad == 5:
schema[pick(["version", "Narrow", "quote"])] = pick([True, "1"])
elif bad == 6:
schema["class"] = pick(["REPORT", "findings", None])
elif bad == 7:
fields.append(dict(fields[0]))
elif bad == 8:
fields[0]["quote"] = pick(["false", 0, None])
else:
schema["confidential"] = pick(["no", 1, None])
return {"schema": shuffled(schema)}
GEN = [("canon", gen_canon), ("draw", gen_draw),
("reconcile", gen_reconcile), ("stage", gen_stage),
("qmatch", gen_qmatch), ("qcheck", gen_qcheck),
("source", gen_source), ("hop", gen_hop), ("stop", gen_stop),
("schema_canon", gen_schema), ("schema_digest", gen_schema)]
cases = []
for fn, g in GEN:
for _ in range(count):
cases.append({"fn": fn, "args": g()})
CASES_PATH = os.environ.get("TPC_DIFF_CASES") # named: kept, for crosspy.py
if not CASES_PATH:
fd, CASES_PATH = tempfile.mkstemp(prefix="tpc_diff_", suffix=".json") # outside the tree
os.close(fd)
with open(CASES_PATH, "w", encoding="utf-8") as f:
json.dump(cases, f)
py = [run_case(c["fn"], c["args"]) for c in cases]
here = os.path.dirname(os.path.abspath(__file__))
php = json.loads(subprocess.run(["php", os.path.join(here, "diff_falcon.php"), CASES_PATH],
capture_output=True, text=True,
check=True).stdout)
if not os.environ.get("TPC_DIFF_CASES"):
os.remove(CASES_PATH)
assert len(php) == len(py)
first = None
for fn, _ in GEN:
idx = [i for i, c in enumerate(cases) if c["fn"] == fn]
diffs = [i for i in idx if py[i] != php[i]]
cover = {}
for i in idx:
out = py[i]
if out.startswith("ERROR"):
keys = ["ERROR"]
elif fn in ("schema_canon", "schema_digest"):
keys = ["OK"]
elif fn == "canon":
keys = ["OK:" + cases[i]["args"]["spec"]["type"]]
elif fn == "draw":
keys = [out.split("\n")[0][5:]]
elif fn == "stage":
keys = [out.split("\n")[0][6:] +
("+DORMANT" if "dormant|true" in out else "")]
elif fn in ("qmatch", "qcheck"):
keys = [out[6:]]
elif fn in ("source", "hop"):
keys = [":".join(out.split("\n")[0].split("|")[1:4:2]
if out.startswith("source|ENTRY")
else out.split("\n")[0].split("|")[1:3])]
if fn == "hop":
keys = [keys[0] + ":" + out.split("\n")[-1][4:]]
elif fn == "stop":
ls = out.split("\n")
keys = [ls[0][5:] + ("+ALONE" if ls[2] != "alone|-" else "")]
else:
keys = [out.split("\n")[-1].split("|")[1]]
keys += sorted({ln.split("|")[2] for ln in out.split("\n")
if ln.startswith("item|")})
keys += sorted({"MALFORMED:" + ln.split("|")[4]
for ln in out.split("\n")
if "|MALFORMED|" in ln})
for k in keys:
cover[k] = cover.get(k, 0) + 1
print("seed %d %-9s cases %d differences %d" % (seed, fn, len(idx),
len(diffs)))
print(" " + ", ".join("%s %d" % (k, cover[k]) for k in sorted(cover)))
if diffs and first is None:
first = diffs[0]
if first is not None:
print(json.dumps(cases[first], ensure_ascii=True))
print(py[first])
print("---")
print(php[first])