All checks were successful
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 8s
wai-app and Create / wai-app · does this change touch it (push) Successful in 17s
WAI Studio / does this change touch it (push) Successful in 19s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 29s
wai-fleet service / does this change touch it (push) Successful in 36s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 39s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 39s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 37s
joule-code conformance gate / joule-moq-relay, its own energy on a counter (macos) (push) Successful in 1m14s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 49s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 59s
wai-fleet service / service · macos (push) Successful in 54s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 1m5s
wai-fleet service / service · linux (push) Successful in 2m5s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 4m42s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 4m46s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 5m39s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 5m57s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 7m26s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 8m44s
joule-code conformance gate / joule-moq-relay, every target (push) Successful in 11m37s
joule-code conformance gate / Build + round-trip C1-C4 vectors (push) Successful in 12m59s
WAI Studio / Studio in WebKit · linux (push) Successful in 13m38s
joule-code conformance gate / joule-moq-ingest, every target (push) Successful in 16m34s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 16m37s
wai release binaries build on Windows / the seven release binaries · windows (push) Successful in 18m25s
wai-release-windows-build.yml's rehearsal failed at e694cb096 (run 28665): wai doctor, run from
the staged archive by archive_doctor.py, reported stack.wai-relay and stack.wai-room "not
checked: it did not finish within 6 s", and the port group too. No check hangs. The doctor runs
each group of checks on a thread of its own and takes what has finished by its 6 s for the whole
run; the three groups that did not finish are the three that start a program (wai-relay.exe and
wai-room.exe asked --version --json, and tasklist.exe for the holder of port 8080), and every
group that starts none finished. The first start of a program written moments ago waits on the
system's scan of a new executable: on cbio-device-0000, Microsoft Defender's real-time protection
on, a fresh copy of the 0.6.0 release's wai-relay.exe (11.8 MB) took 619 and 401 ms to start the
first time and 16 and 11 ms the second, wai-room.exe 425 and 398 ms against 11, wai-ingest.exe
672 and 615 ms against 11 to 13; the doctor from a fresh unpack took 2.5 s cold and 0.6 s warm.
The rehearsal stages debug builds, larger, and runs the doctor right after building them, the
host loaded: its quickstart's first starts of four of them took 4.6 to 9.2 s in the four runs
that passed (2.9 s for the release's), and in the run that failed the doctor's first starts of
two of them, with tasklist beside, passed 6 s. The doctor's spawn of a program is outside the 3 s
it gives the program to answer, so only the 6 s caught it.
The 6 s is the doctor's bound for a person at a prompt, and it stays. archive_doctor.py now starts
bin/wai-relay and bin/wai-room once each, as the doctor asks them, before the doctor, prints how
long each took, and fails when one does not end within --timeout (120 s): a program that hangs,
not a scan. Its exit status is the doctor's to judge, as before.
Checks: archive_doctor_test.py passes with two cases more (a slow first start, asked as the
doctor asks; a first start that does not end, failed at --timeout 2); the new archive_doctor.py
run on the Windows runner over a fresh unpack of wai-0.6.0-x86_64-pc-windows-msvc.zip prints
first starts of 0.5 s each and passes its six required checks.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
169 lines
9 KiB
Python
169 lines
9 KiB
Python
#!/usr/bin/env python3
|
|
"""archive_doctor_test.py: archive_doctor.py passes an archive whose install and stack checks pass,
|
|
and fails each way one can not.
|
|
|
|
Each case makes a directory shaped like an unpacked archive, its bin/wai a stand-in that prints a
|
|
given report (and notes the PATH it was given), and states the exit status the check must give and
|
|
a line its output must hold. Unix only (the stand-in is a shell script); the check itself also runs
|
|
on Windows. Standard library only.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import shutil
|
|
import stat
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
CHECK = os.path.join(HERE, "archive_doctor.py")
|
|
REQUIRED = ["install.version", "install.path", "stack.wai-relay", "stack.wai-room", "stack.web", "stack.studio"]
|
|
|
|
|
|
# Where each check that names a path finds its piece, under the archive.
|
|
WHERE = {"install.version": "bin/wai", "stack.wai-relay": "bin/wai-relay", "stack.wai-room": "bin/wai-room", "stack.web": "share/wai/web", "stack.studio": "share/wai/studio"}
|
|
|
|
|
|
def envelope(states, root, paths=None):
|
|
paths = dict({i: os.path.join(root, w) for i, w in WHERE.items()}, **(paths or {}))
|
|
checks = [{"id": i, "state": s, "message": f"{i} said {s}", "fix": None, "facts": {"path": paths.get(i)} if i in paths else None} for i, s in states.items()]
|
|
data = {"result": "failed", "counts": {}, "checks": checks}
|
|
v = {"wai": "envelope/1", "op": "toolchain.doctor", "ok": False, "data": data}
|
|
return json.dumps(v)
|
|
|
|
|
|
def healthy(**changes):
|
|
states = {i: "pass" for i in REQUIRED}
|
|
states.update({"port.8080": "fail", "browser.studio": "not-checked", "keystore.directory": "fail", "keychain.unlocked": "not-checked"})
|
|
states.update(changes)
|
|
return states
|
|
|
|
|
|
def archive(tmp, body):
|
|
"""A directory shaped like an unpacked archive whose bin/wai runs `body` (sh), or `body(root)`."""
|
|
root = tempfile.mkdtemp(dir=tmp)
|
|
os.makedirs(os.path.join(root, "bin"))
|
|
if callable(body):
|
|
body = body(root)
|
|
wai = os.path.join(root, "bin", "wai")
|
|
with open(wai, "w") as f:
|
|
f.write("#!/bin/sh\nprintf '%s\\n' \"$PATH\" > \"$(dirname \"$0\")/../path-given\"\n" + body + "\n")
|
|
os.chmod(wai, os.stat(wai).st_mode | stat.S_IXUSR)
|
|
return root
|
|
|
|
|
|
def program(root, name, body):
|
|
"""bin/<name> in the archive at `root`, a stand-in that runs `body` (sh)."""
|
|
p = os.path.join(root, "bin", name)
|
|
with open(p, "w") as f:
|
|
f.write("#!/bin/sh\n" + body + "\n")
|
|
os.chmod(p, os.stat(p).st_mode | stat.S_IXUSR)
|
|
|
|
|
|
def printing(doc):
|
|
return "cat <<'EOF'\n" + doc + "\nEOF"
|
|
|
|
|
|
def run(args):
|
|
"""The check's exit status and output; a check that does not end within 60 s is a status of its
|
|
own, so that a hang fails its case rather than the whole test."""
|
|
try:
|
|
p = subprocess.run([sys.executable, "-I", CHECK] + args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=60)
|
|
except subprocess.TimeoutExpired:
|
|
return "no end within 60 s", ""
|
|
return p.returncode, p.stdout
|
|
|
|
|
|
def main():
|
|
tmp = tempfile.mkdtemp()
|
|
fails = 0
|
|
|
|
def expect(what, args, code, needle):
|
|
nonlocal fails
|
|
got, out = run(args)
|
|
if got != code or needle not in out:
|
|
fails += 1
|
|
print(f"FAIL {what}: exit {got} (wanted {code}); '{needle}' {'present' if needle in out else 'absent'}")
|
|
print("\n".join(" " + l for l in out.splitlines()))
|
|
else:
|
|
print(f"ok {what}")
|
|
|
|
try:
|
|
a = archive(tmp, lambda r: printing(envelope(healthy(), r)))
|
|
expect("every install and stack check passes; ports, browsers and the keystore are printed, not required", [a], 0, "every required check passed (6)")
|
|
given = open(os.path.join(a, "path-given")).read().split(os.pathsep)[0].strip()
|
|
if given != os.path.join(os.path.abspath(a), "bin"):
|
|
fails += 1
|
|
print(f"FAIL the archive's bin is not first on PATH: {given}")
|
|
else:
|
|
print("ok the archive's bin is first on the PATH wai is given")
|
|
for i in REQUIRED:
|
|
a = archive(tmp, lambda r: printing(envelope(healthy(**{i: "fail"}), r)))
|
|
expect(f"{i} fails", [a], 1, f"FAIL {i} is fail")
|
|
a = archive(tmp, lambda r: printing(envelope(healthy(**{"stack.wai-relay": "not-checked"}), r)))
|
|
expect("a stack check not checked is no pass", [a], 1, "FAIL stack.wai-relay is not-checked")
|
|
a = archive(tmp, lambda r: printing(envelope(healthy(**{"stack.files": "not-checked"}), r)))
|
|
expect("another stack check, not one of the six, is held too", [a], 1, "FAIL stack.files is not-checked")
|
|
states = healthy()
|
|
del states["stack.web"]
|
|
a = archive(tmp, lambda r: printing(envelope(states, r)))
|
|
expect("a required check missing from the report", [a], 1, "FAIL stack.web is not in the report")
|
|
a = archive(tmp, "echo 'wai 0.4.0'")
|
|
expect("something else than an envelope", [a], 1, "printed no envelope")
|
|
a = archive(tmp, printing(json.dumps({"wai": "envelope/1", "op": "toolchain.show", "ok": True, "data": {}})))
|
|
expect("another operation's envelope", [a], 1, "printed another document")
|
|
a = archive(tmp, "exec sleep 300")
|
|
expect("a wai that does not answer", [a, "--timeout", "2"], 1, "did not end within 2")
|
|
a = archive(tmp, "exit 7")
|
|
expect("a wai that prints nothing", [a], 1, "exited 7 and printed no envelope")
|
|
# The relay and the room service, each started once before the doctor, as it asks them.
|
|
a = archive(tmp, lambda r: printing(envelope(healthy(), r)))
|
|
program(a, "wai-relay", "sleep 1; echo \"$@\" > \"$(dirname \"$0\")/../relay-args\"")
|
|
program(a, "wai-room", "exit 3")
|
|
expect("a slow first start, before the doctor, and a program that exits as the doctor will judge", [a], 0, "first start of bin/wai-relay: 1.")
|
|
said = open(os.path.join(a, "relay-args")).read().strip() if os.path.exists(os.path.join(a, "relay-args")) else None
|
|
if said != "--version --json":
|
|
fails += 1
|
|
print(f"FAIL the relay's first start was not asked as the doctor asks it: {said}")
|
|
else:
|
|
print("ok the relay's first start is asked as the doctor asks it")
|
|
a = archive(tmp, lambda r: printing(envelope(healthy(), r)))
|
|
program(a, "wai-room", "exec sleep 300")
|
|
expect("a first start that does not end", [a, "--timeout", "2"], 1, "FAIL the first start of bin/wai-room --version --json did not end within 2 s")
|
|
# A piece found outside the archive: a relay elsewhere on PATH, Studio's files from WAI_STUDIO.
|
|
elsewhere = tempfile.mkdtemp(dir=tmp)
|
|
a = archive(tmp, lambda r: printing(envelope(healthy(), r, {"stack.wai-relay": os.path.join(elsewhere, "bin", "wai-relay")})))
|
|
expect("a relay that passed from outside the archive", [a], 1, "FAIL stack.wai-relay passed with")
|
|
a = archive(tmp, lambda r: printing(envelope(healthy(), r, {"stack.studio": os.path.join(elsewhere, "studio")})))
|
|
expect("Studio's files from outside the archive", [a], 1, "which is not in the archive's share/")
|
|
a = archive(tmp, lambda r: printing(envelope(healthy(), r, {"stack.web": os.path.join(r, "bin", "web")})))
|
|
expect("the room client's files in the archive, and not under share/", [a], 1, "FAIL stack.web passed with")
|
|
a = archive(tmp, lambda r: printing(envelope(healthy(), r, {"install.version": None})))
|
|
expect("a passing check that names no path", [a], 1, "FAIL install.version passed with None")
|
|
empty = tempfile.mkdtemp(dir=tmp)
|
|
expect("a directory with no bin/wai", [empty], 2, "no bin/wai in it")
|
|
expect("no archive named", [], 2, "name one unpacked archive")
|
|
a = archive(tmp, lambda r: printing(envelope(healthy(**{"browser.conformance": "pass"}), r)))
|
|
expect("--wai with --require: the named check passes", ["--wai", os.path.join(a, "bin", "wai"), "--require", "browser.conformance"], 0, "every required check passed (1)")
|
|
a = archive(tmp, lambda r: printing(envelope(healthy(**{"browser.conformance": "fail", "stack.web": "fail"}), r)))
|
|
expect("--wai with --require: the named check fails, and only it is required", ["--wai", os.path.join(a, "bin", "wai"), "--require", "browser.conformance"], 1, "FAIL browser.conformance is fail")
|
|
got, out = run(["--wai", os.path.join(a, "bin", "wai"), "--require", "browser.conformance"])
|
|
if "stack.web is fail" in out:
|
|
fails += 1
|
|
print("FAIL --wai held a check it was not asked to")
|
|
else:
|
|
print("ok --wai holds the checks it is asked to, and no others")
|
|
expect("--wai with no --require", ["--wai", os.path.join(a, "bin", "wai")], 2, "--wai takes --require")
|
|
finally:
|
|
shutil.rmtree(tmp, ignore_errors=True)
|
|
if fails:
|
|
print(f"archive_doctor_test: {fails} case(s) failed")
|
|
return 1
|
|
print("archive_doctor_test: every case gave its stated status")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|