Some checks failed
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 10s
wai-audit collector and player integration / does this change touch it (push) Successful in 43s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 1m0s
wai-fleet service / does this change touch it (push) Successful in 1m0s
wai-app and Create / wai-app · does this change touch it (push) Successful in 1m1s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 1m3s
WAI operation catalogue / does this change touch it (push) Successful in 1m2s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 1m8s
wai-ledger service / does this change touch it (push) Successful in 1m2s
wai-fleet service / service · macos (push) Successful in 45s
wai-quantum for JavaScript / does this change touch it (push) Successful in 1m2s
wai-sink-android native sink library / does this change touch it (push) Successful in 48s
wai-sink-swift native sink package / does this change touch it (push) Successful in 46s
WAI Studio / does this change touch it (push) Successful in 46s
wai-quantum studio and desktop / does this change touch it (push) Successful in 49s
wai-audit collector and player integration / collector · linux (push) Successful in 1m20s
wai-ledger service / service · macos (push) Successful in 1m2s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 1m2s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 1m26s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 1m21s
wai-ledger service / service · linux (push) Failing after 1m50s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 3m15s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 2m9s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m54s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 3m44s
wai-fleet service / service · linux (push) Successful in 3m1s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 3m12s
wai-quantum studio and desktop / studio · browser (push) Successful in 4m9s
wai-quantum for JavaScript / javascript · linux (push) Successful in 4m29s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 4m28s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 5m26s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 6m10s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 5m45s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 8m3s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 7m22s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 7m23s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 9m59s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 10m52s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 13m34s
WAI Studio / Studio in WebKit · linux (push) Successful in 12m47s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 15m58s
Every MUST test could pass on a first GET, which a cache relays from the origin, so a cache that broke §9 rule 3 only on what it stored passed the suite. C14 asks again, a second after the first GET, for an object, a head proof, an alias, a live-edge part, a receipt file, a playlist receipt, a key log and a parameter set, and holds each answer to the origin's bytes, ETag, Content-Type, Cache-Control and Content-Length, with no Vary. Every 206 is held to the origin's headers too. Three negative controls show the driver catches those faults: stored changes the headers of every answer after a path's first, ranged those of every 206, and reflip a byte of every answer after a path's first. S3 says how many of C14's repeats the cache answered without reaching the origin and S4 how many of the run's requests reached it; the summary marks a labelled cache whose store answered none as a relay. A WAI_CACHE_DIR whose labels file is empty fails the run as an absent one does, and a labelled cache that does not start has a row in the summary. wai http origin sends no Date while its clock reads before the epoch (RFC 9110 §6.6.1). Co-Authored-By: Claude Opus 5.5 <[email protected]>
405 lines
19 KiB
Python
Executable file
405 lines
19 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, rule 3 is held to the origin's own headers, and 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"
|
|
|
|
# A cache may finish storing an object just after it has answered for it, so C14 waits this
|
|
# long before it asks again.
|
|
SETTLE_S = 1
|
|
|
|
|
|
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)
|
|
|
|
|
|
SENT = {} # the requests this run made, by the base they went to
|
|
|
|
|
|
def send(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 request(base, path, headers=None, method="GET"):
|
|
SENT[base] = SENT.get(base, 0) + 1
|
|
return send(base, path, headers, method)
|
|
|
|
|
|
def hits(origin, path):
|
|
if origin is None:
|
|
return None
|
|
r = send(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 = {}
|
|
every_path = ([path(k) for k in obj] + [path(k) + ".hp" for k in obj] + [st["alias"]["path"], st["live"]["path"], st["index"],
|
|
st["receipt"], st["playlist_receipt"], st["key_log"], st["set"]])
|
|
at_start = {p: hits(origin, p) for p in every_path}
|
|
counted = None not in at_start.values()
|
|
references = {}
|
|
repeats = {}
|
|
|
|
def reference(p):
|
|
"""What rule 3 holds an answer for p to: the origin's own answer, and with no
|
|
--origin the first answer the cache gave."""
|
|
if p not in references:
|
|
references[p] = request(origin or base, p)
|
|
return references[p]
|
|
|
|
def headers_kept(r, p, what):
|
|
ref = reference(p)
|
|
for name in ("etag", "content-type", "cache-control", "vary", "access-control-allow-origin"):
|
|
need(r.h(name) == ref.h(name), "%s: %s %s, the origin's %s" % (what, name, r.h(name), ref.h(name)))
|
|
|
|
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, p=None):
|
|
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))
|
|
headers_kept(r, p or path(k), what)
|
|
|
|
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, p=p)
|
|
|
|
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 c14():
|
|
"""What a cache adds is its store: every test above can pass on a first GET alone, so
|
|
here each kind of resource is asked for again once the cache has had time to store
|
|
it. Every repeat is made before anything is judged, so S3 always has all of them."""
|
|
kept = [path("O2"), path("O3"), path("O5"), path("O3") + ".hp", st["alias"]["path"], st["live"]["path"],
|
|
st["receipt"], st["playlist_receipt"], st["key_log"], st["set"]]
|
|
for p in kept:
|
|
reference(p)
|
|
request(base, p)
|
|
request(base, path("O4"))
|
|
time.sleep(SETTLE_S)
|
|
for p in kept:
|
|
before = hits(origin, p)
|
|
r = request(base, p)
|
|
repeats[p] = (r, before == hits(origin, p))
|
|
for p in kept:
|
|
r, ref = repeats[p][0], reference(p)
|
|
need(r.status == 200 and r.body == ref.body, "%s, again: status %d, %d bytes, the origin's %d" % (p, r.status, len(r.body), len(ref.body)))
|
|
need(r.h("content-encoding") in (None, "identity"), "%s, again: Content-Encoding %s" % (p, r.h("content-encoding")))
|
|
need(r.h("content-length") == ref.h("content-length"), "%s, again: Content-Length %s" % (p, r.h("content-length")))
|
|
headers_kept(r, p, "%s, again" % p)
|
|
range_ok(request(base, path("O4"), {"Range": "bytes=1009-3000"}), "O4", 1009, 3000, "range, again")
|
|
|
|
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"
|
|
|
|
def s3():
|
|
if not counted:
|
|
return "not run: no hit counter"
|
|
return "%d of %d repeat GETs answered from the store" % (sum(1 for _, stored in repeats.values() if stored), len(repeats))
|
|
|
|
def s4():
|
|
if not counted:
|
|
return "not run: no hit counter"
|
|
reached = sum(hits(origin, p) - n for p, n in at_start.items())
|
|
# The references went to the origin itself, past the cache.
|
|
return "%d of %d requests reached the origin" % (reached if base == origin else reached - SENT.get(origin, 0), SENT.get(base, 0))
|
|
|
|
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, "C14": c14, "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, "S3": s3, "S4": s4}
|
|
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:]))
|