261030.4 · 6182a9aa802e833d238ab04b403b428f42fe37f9
A black-box suite runs the built binary, starting with the 404 coin
make blackbox runs tests/ with pytest and httpx against a server in a throwaway home, on --public and on the form wwwvm's unit uses. Cases are TOML files named after the decision they check.
The coin shows on both forms for a path a served repo lacks. The cache message appears only without --public, for a path that names no repo.
reading declarations
Changes to tests/harness.py. All files
@@ -0,0 +1,154 @@+"""Run the binary named by TEST_BIN in a throwaway home and ask it for pages."""++import os+import signal+import socket+import subprocess+import time+from pathlib import Path++import httpx++GIT_DATE = "2026-01-02T03:04:05Z"++# Every repo the seeded cache holds. The host never resolves, so no case+# reaches a network.+SEEDED = {"forge.invalid/acct/r": {"README.md": "# r\n"}}+++def binary():+ path = os.environ.get("TEST_BIN")+ if not path:+ raise RuntimeError("TEST_BIN must name the binary; make blackbox sets it")+ return Path(path).resolve()+++def environment(home):+ """The whole environment of a run: nothing leaks in from the caller's."""+ return {+ "PATH": os.environ["PATH"],+ "HOME": str(home),+ "XDG_CACHE_HOME": str(home / "cache"),+ "XDG_CONFIG_HOME": str(home / "config"),+ "XDG_STATE_HOME": str(home / "state"),+ "GIT_CONFIG_GLOBAL": os.devnull,+ "GIT_CONFIG_NOSYSTEM": "1",+ "TZ": "UTC",+ }+++def git(cwd, *args):+ env = {+ "PATH": os.environ["PATH"],+ "GIT_CONFIG_GLOBAL": os.devnull,+ "GIT_CONFIG_NOSYSTEM": "1",+ "GIT_AUTHOR_NAME": "a",+ "GIT_AUTHOR_EMAIL": "a@forge.invalid",+ "GIT_AUTHOR_DATE": GIT_DATE,+ "GIT_COMMITTER_NAME": "a",+ "GIT_COMMITTER_EMAIL": "a@forge.invalid",+ "GIT_COMMITTER_DATE": GIT_DATE,+ }+ done = subprocess.run(["git", *args], cwd=cwd, env=env, check=True, capture_output=True, text=True)+ return done.stdout.strip()+++def seed(cache, key, files):+ """Lay out one repo as the binary leaves it after a clone.++ A public server fetches only over https from the open internet, so a+ case with no network cannot have it clone. This is the one place the+ suite knows the cache's layout.+ """+ source = cache.parent / "sources" / key+ source.mkdir(parents=True)+ git(source, "init", "-q", "-b", "main")+ for name, text in files.items():+ (source / name).parent.mkdir(parents=True, exist_ok=True)+ (source / name).write_text(text)+ git(source, "add", ".")+ git(source, "commit", "-q", "-m", "first")+ head = git(source, "rev-parse", "HEAD")+ repo = cache / key+ repo.mkdir(parents=True)+ git(repo, "clone", "-q", "--bare", str(source), ".git")+ name = ".wt-" + head[:12]+ git(repo / ".git", "worktree", "add", "-q", "--detach", "--", str(repo / name), head)+ (repo / ".tree").symlink_to(name)+ (repo / ".fetched").touch()+ (repo / ".used").touch()+++def free_port():+ with socket.socket() as s:+ s.bind(("127.0.0.1", 0))+ return s.getsockname()[1]+++class Server:+ """One running binary. origin is where its repos answer."""++ def __init__(self, home, argv, origin, listens):+ self.origin = origin+ self.log = home / "stderr"+ self.proc = subprocess.Popen(+ [str(binary()), *argv],+ cwd=home,+ env=environment(home),+ stdin=subprocess.DEVNULL,+ stdout=subprocess.DEVNULL,+ stderr=self.log.open("w"),+ )+ self._wait(listens)++ def _wait(self, addr):+ end = time.monotonic() + 10+ while time.monotonic() < end:+ if self.proc.poll() is not None:+ raise RuntimeError(f"exit {self.proc.returncode}: {self.log.read_text()}")+ try:+ socket.create_connection(addr, timeout=0.2).close()+ return+ except OSError:+ time.sleep(0.02)+ raise TimeoutError(f"nothing listens on {addr}: {self.log.read_text()}")++ def get(self, path):+ """The page at path once any read it set going has ended."""+ end = time.monotonic() + 30+ while True:+ page = httpx.get(self.origin + path, trust_env=False, timeout=30)+ if page.status_code != 202 or time.monotonic() > end:+ return page+ time.sleep(0.1)++ def stop(self):+ self.proc.send_signal(signal.SIGTERM)+ try:+ self.proc.wait(10)+ except subprocess.TimeoutExpired:+ self.proc.kill()+ self.proc.wait()+++# public: the flag the README names for the open web.+# default: a reader's server, as wwwvm's unit runs it, whose online repos+# answer on the .2 beside the user's default address.+def start(form, home):+ """Start the binary in one of the forms a server runs in."""+ cache = home / "cache" / binary().name+ for key, files in SEEDED.items():+ seed(cache, key, files)+ port = free_port()+ if form == "public":+ return Server(home, ["--public", "--disk=1G", "-a", f"127.0.0.1:{port}"],+ f"http://127.0.0.1:{port}", ("127.0.0.1", port))+ if form == "default":+ n = os.getuid() - 1000+ if not 0 <= n <= 255:+ raise OSError(f"uid {os.getuid()} has no default address")+ tree = home / "tree"+ tree.mkdir()+ return Server(home, ["--foreground", "--no-open", "--no-bug", "--disk=1G", "-a", f"127.31.{n}.1:{port}", str(tree)],+ f"http://127.31.{n}.2:{port}", (f"127.31.{n}.2", port))+ raise ValueError(form)