open-standards/wai/cache-conformance/driver.py
Transaction Science 39d9894e73 wai: cache conformance (public part), the wai-http workflow, and the CI wiring
W3-7, public part. cache-conformance/: tests.json (C1–C13, R1–R5 as MUST
tests of §9 and the sidecar rules, S1–S2 informative), driver.py (stages the
corpus behind an origin and runs the tests against a cache; standard library
and _blake3.py only), mutators.py (flip, gzip, truncate, vary: one §9 defect
each), run.sh (the reference origin must pass every MUST test; each control
must be caught by the tests named for it; labelled caches run where an
environment provides /opt/wai-cache, and the summary says when none did) and
a README. Reports name a cache by label, package version and configuration
digest only. The private harness image is not in this repository.

The reference origin gains what the tests need of it: If-Range by strong
comparison, a live-edge prefix served with a shorter max-age, §8.4's
Cache-Control for the key log and grants, and a request counter
(--count-hits) for the informative tests.

.github/workflows/wai-http.yml: the falsify job (every patch caught, every
Python mutation caught, the differential) and the cache-conformance job
(reports uploaded as an artifact). wai-conformance.yml: `http` joins the
build, the library tests, the corpus loop (wai_http_vectors), the
feature-alone and wasm32 checks; http_verify.py joins the independent
verifiers. wai_http_vectors verify now names a case whose evaluation panics.

http-delivery.md writes byte spans in words where a half-open interval read
as link text to the section-reference check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 08:02:11 -04:00

339 lines
16 KiB
Python
Executable file

#!/usr/bin/env python3
"""Cache conformance for HTTP delivery (extensions/http-delivery.md §9).
Runs tests.json against an HTTP cache that fronts an origin serving the objects of
wai/http-conformance/, and reports, per test, whether the cache met it. Standard library
only, and ../claim-conformance/_blake3.py to check each object's bytes against its
content hash; it shares no code with wai-rs.
python3 driver.py stage <http-conformance dir> <root>
lay out the files an origin serves for the tests under <root>
python3 driver.py run --base URL --root <root> [--origin URL] [--label L]
[--version V] [--config FILE] [--report report.json]
run the tests against the cache at URL (the origin itself, for a control)
`run` exits 1 when a MUST test fails, and prints `FAIL <id>: <detail>` for each. With
--origin, the origin's hit counter (`wai http origin --count-hits`) lets the informative
tests see whether a request reached the origin. A report names the cache by its label
and version only.
"""
import hashlib, http.client, json, os, shutil, sys, time, urllib.parse
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance"))
from _blake3 import blake3 # noqa: E402
DOMAIN = b"moq-jwp:object\x01"
def cid_of(b):
return blake3(DOMAIN + b).hex()
# ------------------------------------------------------------------- staging
def stage(corpus, root):
"""The origin's files: the corpus objects and head proofs, an alias, a live-edge
part, a receipt index, a receipt file, a playlist receipt, a key log and a
parameter set named by its digest."""
if os.path.exists(root):
shutil.rmtree(root)
os.makedirs(os.path.join(root, "objects"))
listing = json.load(open(os.path.join(corpus, "objects.json")))
for oid, o in listing.items():
for suffix in ("", ".hp"):
shutil.copyfile(os.path.join(corpus, o["path"] + suffix), os.path.join(root, o["path"] + suffix))
def put(rel, data):
p = os.path.join(root, rel)
os.makedirs(os.path.dirname(p), exist_ok=True)
open(p, "wb").write(data)
o8 = open(os.path.join(corpus, listing["O8"]["path"]), "rb").read()
o9b = open(os.path.join(corpus, listing["O9b"]["path"]), "rb").read()
put("alias/segment-8.wai", o8)
put("live/part-1.wai", o9b)
index = json.dumps({"kind": "wai-receipt-index", "object": listing["O3"]["cid"], "delivery": [], "claims": []}, sort_keys=True).encode()
put(listing["O3"]["path"] + ".receipts", index)
fixture = json.dumps({"kind": "cache-conformance-fixture", "of": "a receipt file"}, sort_keys=True).encode()
put("objects/rcpt/%s.json" % blake3(fixture).hex(), fixture)
playlist = json.dumps({"kind": "cache-conformance-fixture", "of": "a playlist receipt"}, sort_keys=True).encode()
put("objects/mr/%s.json" % blake3(playlist).hex(), playlist)
put(".well-known/wai-keys", json.dumps({"kind": "cache-conformance-fixture", "of": "a key log"}).encode())
pset = open(os.path.join(corpus, "..", "wai-rs", "tests", "vectors", "int_mlicpp", "model.wmb"), "rb").read()
put("sets/" + hashlib.sha256(pset).hexdigest(), pset)
manifest = {
"objects": {oid: {"path": o["path"], "cid": o["cid"], "n": o["n"]} for oid, o in listing.items()},
"alias": {"path": "alias/segment-8.wai", "of": "O8"},
"live": {"path": "live/part-1.wai", "of": "O9b"},
"index": listing["O3"]["path"] + ".receipts",
"receipt": "objects/rcpt/%s.json" % blake3(fixture).hex(),
"playlist_receipt": "objects/mr/%s.json" % blake3(playlist).hex(),
"key_log": ".well-known/wai-keys",
"set": "sets/" + hashlib.sha256(pset).hexdigest(),
}
open(os.path.join(root, "stage.json"), "w").write(json.dumps(manifest, indent=1) + "\n")
print("staged %d objects and their sidecars under %s" % (len(listing), root))
# ------------------------------------------------------------------- requests
class Response:
def __init__(self, status, headers, body):
self.status, self.body = status, body
self.headers = {}
for k, v in headers:
self.headers.setdefault(k.lower(), []).append(v)
def h(self, name):
v = self.headers.get(name.lower())
return None if v is None else ", ".join(v)
def request(base, path, headers=None, method="GET"):
u = urllib.parse.urlsplit(base)
conn = http.client.HTTPConnection(u.hostname, u.port or 80, timeout=30)
prefix = u.path.rstrip("/")
h = {"Connection": "close"}
h.update(headers or {})
conn.request(method, prefix + "/" + path.lstrip("/"), headers=h)
r = conn.getresponse()
body = r.read()
out = Response(r.status, r.getheaders(), body)
conn.close()
return out
def hits(origin, path):
if origin is None:
return None
r = request(origin, "/__hits?path=" + urllib.parse.quote("/" + path.lstrip("/")))
return int(r.body) if r.status == 200 else None
# --------------------------------------------------------------------- tests
class Fail(Exception):
pass
def need(cond, why):
if not cond:
raise Fail(why)
def run_tests(base, root, origin):
st = json.load(open(os.path.join(root, "stage.json")))
obj = {k: open(os.path.join(root, v["path"]), "rb").read() for k, v in st["objects"].items()}
for k, v in st["objects"].items():
if cid_of(obj[k]) != v["cid"]:
raise SystemExit("the staged object %s does not hash to its cid" % k)
path = lambda k: st["objects"][k]["path"]
etag = lambda k: '"%s"' % st["objects"][k]["cid"]
results = {}
def whole_ok(r, k, what):
need(r.status == 200, "%s: status %d" % (what, r.status))
need(r.h("content-encoding") in (None, "identity"), "%s: Content-Encoding %s" % (what, r.h("content-encoding")))
need(r.body == obj[k], "%s: %d bytes that are not the object's %d" % (what, len(r.body), len(obj[k])))
def range_ok(r, k, a, b, what, allow_200=False):
n = len(obj[k])
if allow_200 and r.status == 200:
whole_ok(r, k, what)
return
need(r.status == 206, "%s: status %d" % (what, r.status))
need(r.h("content-encoding") in (None, "identity"), "%s: Content-Encoding %s" % (what, r.h("content-encoding")))
need(r.h("content-range") == "bytes %d-%d/%d" % (a, b, n), "%s: Content-Range %s" % (what, r.h("content-range")))
need(r.body == obj[k][a:b + 1], "%s: the bytes are not the object's [%d, %d]" % (what, a, b))
def c1():
whole_ok(request(base, path("O3")), "O3", "cold")
whole_ok(request(base, path("O3")), "O3", "again")
def c2():
r = request(base, path("O2"))
whole_ok(r, "O2", "GET")
need(r.h("etag") == etag("O2"), "ETag %s" % r.h("etag"))
need((r.h("content-type") or "").startswith("application/wai"), "Content-Type %s" % r.h("content-type"))
need(r.h("cache-control") == "public, max-age=31536000, immutable, no-transform", "Cache-Control %s" % r.h("cache-control"))
need(r.h("content-length") == str(len(obj["O2"])), "Content-Length %s" % r.h("content-length"))
def c3():
request(base, path("O4"))
range_ok(request(base, path("O4"), {"Range": "bytes=1009-3000"}), "O4", 1009, 3000, "range")
def c4():
n = len(obj["O7"])
range_ok(request(base, path("O7"), {"Range": "bytes=1009-2999"}), "O7", 1009, 2999, "cold range", allow_200=True)
range_ok(request(base, path("O7"), {"Range": "bytes=0-1008"}), "O7", 0, 1008, "head range", allow_200=True)
need(n > 0, "empty")
def c5():
n = len(obj["O9c"])
request(base, path("O9c"))
range_ok(request(base, path("O9c"), {"Range": "bytes=-100"}), "O9c", n - 100, n - 1, "suffix")
range_ok(request(base, path("O9c"), {"Range": "bytes=%d-" % (n - 50)}), "O9c", n - 50, n - 1, "open")
def c6():
n = len(obj["O9c"])
r = request(base, path("O9c"), {"Range": "bytes=%d-" % n})
need(r.status == 416, "status %d" % r.status)
need(r.h("content-range") == "bytes */%d" % n, "Content-Range %s" % r.h("content-range"))
def c7():
for enc in ("gzip, deflate, br", "br", "zstd, gzip"):
whole_ok(request(base, path("O9a"), {"Accept-Encoding": enc}), "O9a", "Accept-Encoding %s" % enc)
def c8():
request(base, path("OD"))
range_ok(request(base, path("OD"), {"Range": "bytes=1009-3593", "If-Range": etag("OD")}), "OD", 1009, 3593, "matching If-Range")
whole_ok(request(base, path("OD"), {"Range": "bytes=1009-3593", "If-Range": "W/" + etag("OD")}), "OD", "weak If-Range")
whole_ok(request(base, path("OD"), {"Range": "bytes=1009-3593", "If-Range": '"%s"' % ("0" * 64)}), "OD", "other If-Range")
def c9():
a = request(base, path("O5"), {"User-Agent": "conformance-a", "Accept-Language": "en", "Accept": "*/*"})
b = request(base, path("O5"), {"User-Agent": "conformance-b", "Accept-Language": "fr", "Accept": "application/wai"})
whole_ok(a, "O5", "first")
whole_ok(b, "O5", "second")
need(a.h("vary") is None and b.h("vary") is None, "Vary %s / %s" % (a.h("vary"), b.h("vary")))
def c10():
p = path("O3") + ".hp"
r = request(base, p)
need(r.status == 200 and r.body == open(os.path.join(root, p), "rb").read(), "the head proof differs (status %d)" % r.status)
need(r.h("content-encoding") in (None, "identity"), "Content-Encoding %s" % r.h("content-encoding"))
need(r.h("cache-control") == "public, max-age=31536000, immutable, no-transform", "Cache-Control %s" % r.h("cache-control"))
def c11():
r = request(base, st["alias"]["path"])
whole_ok(r, st["alias"]["of"], "alias")
need(r.h("etag") == etag(st["alias"]["of"]), "ETag %s" % r.h("etag"))
def c12():
k, p = st["live"]["of"], st["live"]["path"]
r = request(base, p)
whole_ok(r, k, "live part")
cc = r.h("cache-control") or ""
need("max-age=31536000" not in cc and "max-age=" in cc, "Cache-Control %s" % cc)
range_ok(request(base, p, {"Range": "bytes=10-99"}), k, 10, 99, "live range", allow_200=True)
def c13():
r = request(base, path("O1"), {"Origin": "https://reader.example"})
whole_ok(r, "O1", "with Origin")
need(r.h("access-control-allow-origin") == "*", "Access-Control-Allow-Origin %s" % r.h("access-control-allow-origin"))
need("origin" not in (r.h("vary") or "").lower(), "Vary %s" % r.h("vary"))
def r1():
p = st["index"]
first = request(base, p)
need(first.status == 200, "status %d" % first.status)
changed = json.dumps({"kind": "wai-receipt-index", "changed_at": time.time()}).encode()
full = os.path.join(root, p)
old = open(full, "rb").read()
open(full, "wb").write(changed)
try:
second = request(base, p)
finally:
open(full, "wb").write(old)
need(second.status == 200 and second.body == changed, "the index came back unchanged")
def fetch_kept(p, cc=None):
r = request(base, p)
need(r.status == 200 and r.body == open(os.path.join(root, p), "rb").read(), "%s differs (status %d)" % (p, r.status))
need(r.h("content-encoding") in (None, "identity"), "Content-Encoding %s" % r.h("content-encoding"))
if cc is not None:
need(r.h("cache-control") == cc, "Cache-Control %s" % r.h("cache-control"))
return r
def kept(p, cc=None):
fetch_kept(p, cc)
imm = "public, max-age=31536000, immutable, no-transform"
def r5():
r = fetch_kept(st["set"], imm)
need(hashlib.sha256(r.body).hexdigest() == st["set"].rsplit("/", 1)[1], "the set does not hash to its name")
def s1():
if origin is None:
return "not run: no hit counter"
p = path("O8")
request(base, p)
before = hits(origin, p)
request(base, p)
return "stored" if hits(origin, p) == before else "not stored"
def s2():
if origin is None:
return "not run: no hit counter"
p = path("O9b")
request(base, p)
before = hits(origin, p)
request(base, p, {"Range": "bytes=100-199"})
return "answered from store" if hits(origin, p) == before else "forwarded"
table = {"C1": c1, "C2": c2, "C3": c3, "C4": c4, "C5": c5, "C6": c6, "C7": c7, "C8": c8, "C9": c9,
"C10": c10, "C11": c11, "C12": c12, "C13": c13, "R1": r1,
"R2": lambda: kept(st["receipt"], imm), "R3": lambda: kept(st["playlist_receipt"], imm),
"R4": lambda: kept(st["key_log"], "max-age=300, no-transform"), "R5": r5, "S1": s1, "S2": s2}
tests = json.load(open(os.path.join(HERE, "tests.json")))["tests"]
out = []
for t in tests:
try:
detail = table[t["id"]]()
out.append({"id": t["id"], "name": t["name"], "level": t["level"], "pass": True, "detail": detail})
except Fail as e:
out.append({"id": t["id"], "name": t["name"], "level": t["level"], "pass": False, "detail": str(e)})
except (OSError, http.client.HTTPException) as e:
out.append({"id": t["id"], "name": t["name"], "level": t["level"], "pass": False, "detail": "transport: %s" % e})
return out
def requirements_md(report):
lines = ["# Cache configuration: %s" % report["cache_label"], "",
"Package version: %s. Configuration SHA-256: %s." % (report["package_version"], report["config_sha256"] or "none"), "",
"| test | level | result | detail |", "|---|---|---|---|"]
for r in report["results"]:
res = ("pass" if r["pass"] else "FAIL") if r["level"] == "MUST" else (r["detail"] or "")
lines.append("| %s %s | %s | %s | %s |" % (r["id"], r["name"], r["level"], res, "" if r["pass"] else r["detail"]))
musts = [r for r in report["results"] if r["level"] == "MUST"]
ok = all(r["pass"] for r in musts)
lines += ["", "This configuration %s http-delivery §9 for every MUST test." % ("meets" if ok else "does not meet")]
return "\n".join(lines) + "\n"
def main(argv):
if len(argv) == 3 and argv[0] == "stage":
stage(argv[1], argv[2])
return 0
if not argv or argv[0] != "run":
print(__doc__)
return 2
opt = lambda f, d=None: argv[argv.index(f) + 1] if f in argv else d
base, root = opt("--base"), opt("--root")
if base is None or root is None:
print(__doc__)
return 2
results = run_tests(base, root, opt("--origin"))
cfg = opt("--config")
report = {
"cache_label": opt("--label", "unlabelled"),
"package_version": opt("--version", "n/a"),
"config_sha256": hashlib.sha256(open(cfg, "rb").read()).hexdigest() if cfg else None,
"results": results,
}
if opt("--report"):
open(opt("--report"), "w").write(json.dumps(report, indent=1) + "\n")
open(os.path.join(os.path.dirname(opt("--report")) or ".", "requirements-%s.md" % report["cache_label"]), "w").write(requirements_md(report))
failed = [r for r in results if r["level"] == "MUST" and not r["pass"]]
for r in results:
if not r["pass"]:
print("FAIL %s: %s" % (r["id"], r["detail"]))
elif r["level"] != "MUST":
print("info %s: %s" % (r["id"], r["detail"]))
print("%s: %d/%d MUST tests pass" % (report["cache_label"], sum(1 for r in results if r["level"] == "MUST" and r["pass"]),
sum(1 for r in results if r["level"] == "MUST")))
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))