Files
rune/scripts/gen_forge_client_truth.py
T

347 lines
15 KiB
Python

#!/usr/bin/env python3
"""Truth generator for the forge_client dichotomy port (#2479 tranche, GO).
Runs the LIVE Python oracle (src/rune/forge_client.py) with the httpx
transport fully scripted, exercises every public function and every branch,
and freezes inputs + expected outputs to src/go/forge_client/truth.json
(consumed VERBATIM by the Go battery).
No clock, no store, no env leakage: every case is hermetic — the transport
is a scripted fake (never a real socket), sleep is a recording fake, and
os.environ is snapshotted/restored around every selection case.
Scripted entry shapes:
{"status": N, "payload": {...}} → FakeResponse(N, payload)
{"exc": "connect" | "connect_timeout" | "read_timeout"} → scripted
httpx exception (connect-phase vs non-connect transport, the
NOISE-vs-SIGNAL axis of _request).
"""
import json
import os
import sys
from pathlib import Path
from unittest.mock import patch
import httpx
REPO = Path(__file__).resolve().parents[1]
OUT = REPO / "src" / "go" / "forge_client" / "truth.json"
sys.path.insert(0, str(REPO / "src"))
import rune.forge_client as fc # noqa: E402
EXC = {
"connect": httpx.ConnectError,
"connect_timeout": httpx.ConnectTimeout,
"read_timeout": httpx.ReadTimeout,
}
rows = []
coverage = set()
def add(name, op, inp, want, covered):
rows.append({"name": name, "op": op, "input": inp, "want": want})
coverage.update(covered)
class FakeResponse:
def __init__(self, status=200, payload=None):
self.status_code = status
self._payload = payload if payload is not None else {}
def json(self):
return self._payload
def raise_for_status(self):
if self.status_code >= 400:
raise httpx.HTTPStatusError(
f"error {self.status_code}",
request=httpx.Request("GET", "https://x"),
response=httpx.Response(self.status_code),
)
def make_fake(method, script, rec):
pending = list(script)
def fake(url, **kw):
rec["requests"].append(
{
"method": method,
"url": url,
"headers": kw.get("headers"),
"json": kw.get("json"),
"timeout": kw.get("timeout"),
}
)
if not pending:
raise AssertionError(f"script exhausted at request {len(rec['requests'])}")
entry = pending.pop(0)
if "exc" in entry:
raise EXC[entry["exc"]]("scripted")
return FakeResponse(entry.get("status", 200), entry.get("payload"))
return fake
def blank_rec():
return {"requests": [], "sleeps": [], "error": None}
def finish_rec(rec, result):
return {
"result": result,
"requests": rec["requests"],
"sleeps": rec["sleeps"],
"error": rec["error"],
}
def call_case(name, method_name, script, kwargs, *, url="https://forge.example.com",
token="tok", owner="o", repo="r", covered=()):
rec = blank_rec()
adapter = fc.GiteaForgejoAdapter(url, token, owner=owner, repo=repo)
with patch("rune.forge_client.httpx.get", make_fake("GET", script, rec)), patch(
"rune.forge_client.httpx.post", make_fake("POST", script, rec)
), patch("rune.forge_client.time.sleep", rec["sleeps"].append):
try:
result = getattr(adapter, method_name)(**kwargs)
except Exception as e: # noqa: BLE001 — the kind IS the information
rec["error"] = f"httpx.{type(e).__name__}"
result = None
add(
name,
"call",
{
"method": method_name,
"url": url,
"token": token,
"owner": owner,
"repo": repo,
"script": script,
"kwargs": kwargs,
},
finish_rec(rec, result),
("GiteaForgejoAdapter." + method_name,) + covered,
)
def request_case(name, method, script, *, headers=None, json_body=None, timeout=30,
retries=2, backoff=0.5, covered=()):
rec = blank_rec()
with patch("rune.forge_client.httpx.get", make_fake("GET", script, rec)), patch(
"rune.forge_client.httpx.post", make_fake("POST", script, rec)
):
try:
result = fc._request(
method,
"https://x/api",
headers=headers,
json=json_body,
timeout=timeout,
retries=retries,
backoff=backoff,
sleep=rec["sleeps"].append,
).json()
except Exception as e: # noqa: BLE001
rec["error"] = f"httpx.{type(e).__name__}"
result = None
add(
name,
"request",
{
"method": method,
"url": "https://x/api",
"headers": headers,
"json": json_body,
"timeout": timeout,
"retries": retries,
"backoff": backoff,
"script": script,
},
finish_rec(rec, result),
("_request",) + covered,
)
def select_case(name, config, env, covered=()):
saved = {k: os.environ.get(k) for k in
("FORGE_URL", "FORGE_TOKEN", "FORGE_OWNER", "FORGE_REPO")}
try:
for k in ("FORGE_URL", "FORGE_TOKEN", "FORGE_OWNER", "FORGE_REPO"):
os.environ.pop(k, None)
os.environ.update({k: v for k, v in env.items() if v is not None})
client = fc.create_forge_client(config)
if isinstance(client, fc.NullAdapter):
want = {"adapter": "NullAdapter", "base_url": None, "token": None}
else:
want = {
"adapter": "GiteaForgejoAdapter",
"base_url": client._base_url,
"token": client._token,
}
finally:
for k, v in saved.items():
if v is None:
os.environ.pop(k, None)
else:
os.environ[k] = v
add(name, "select", {"config": config, "env": env}, want,
("create_forge_client",) + covered)
def null_case(name, method_name, args):
client = fc.NullAdapter()
result = getattr(client, method_name)(*args)
add(name, "null", {"method": method_name, "args": args}, result,
("NullAdapter." + method_name,))
# ── create_forge_client: every selection branch ─────────────────────────────
select_case("select:none", None, {})
select_case("select:env-configured", {}, {"FORGE_URL": "https://forge.example.com",
"FORGE_TOKEN": "test_token"})
select_case("select:config-keys",
{"forge.url": "https://cfg.example.com", "forge.token": "cfg_tok",
"forge.owner": "cfg_owner", "forge.repo": "cfg_repo"}, {})
select_case("select:config-beats-env",
{"forge.url": "https://cfg.example.com", "forge.token": "cfg_tok"},
{"FORGE_URL": "https://env.example.com", "FORGE_TOKEN": "env_tok"})
select_case("select:owner-repo-defaults", {},
{"FORGE_URL": "https://forge.example.com", "FORGE_TOKEN": "test_token"})
select_case("select:owner-repo-env", {},
{"FORGE_URL": "https://forge.example.com", "FORGE_TOKEN": "test_token",
"FORGE_OWNER": "env_owner", "FORGE_REPO": "env_repo"})
select_case("select:trailing-slash-strip", {},
{"FORGE_URL": "https://forge.example.com///", "FORGE_TOKEN": "test_token"})
select_case("select:empty-cfg-url-falls-to-env",
{"forge.url": "", "forge.token": ""},
{"FORGE_URL": "https://forge.example.com", "FORGE_TOKEN": "test_token"})
select_case("select:cfg-url-missing-token", {"forge.url": "https://cfg.example.com"}, {})
select_case("select:cfg-token-missing-url", {"forge.token": "cfg_tok"}, {})
select_case("select:owner-empty-env", {},
{"FORGE_URL": "https://forge.example.com", "FORGE_TOKEN": "test_token",
"FORGE_OWNER": ""})
# ── NullAdapter: every no-op shape ──────────────────────────────────────────
null_case("null:create-issue", "create_issue", ["t", "b"])
null_case("null:add-comment", "add_comment", [3, "c"])
null_case("null:get-issue-state", "get_issue_state", [7])
null_case("null:list-open-issues", "list_open_issues", [])
null_case("null:search-issues", "search_issues", ["q"])
# ── GiteaForgejoAdapter: every method + happy paths ─────────────────────────
call_case("call:create-issue", "create_issue",
[{"status": 200, "payload": {"number": 42, "state": "open"}}],
{"title": "Test", "body": "Body"})
call_case("call:create-issue-201", "create_issue",
[{"status": 201, "payload": {"number": 1, "title": "t"}}],
{"title": "t", "body": "b"})
call_case("call:create-issue-4xx", "create_issue",
[{"status": 422, "payload": {}}], {"title": "dup", "body": "b"})
call_case("call:add-comment", "add_comment",
[{"status": 200, "payload": {"id": 9, "body": "c"}}],
{"issue_number": 5, "body": "c"})
call_case("call:get-issue-state", "get_issue_state",
[{"status": 200, "payload": {"number": 9, "state": "open"}}],
{"issue_number": 9})
call_case("call:list-open-issues", "list_open_issues",
[{"status": 200, "payload": [{"number": 1}, {"number": 2}]}], {})
call_case("call:search-single-page", "search_issues",
[{"status": 200, "payload": [{"number": 3, "title": "[auto-failure:cron:Connection]"}]}],
{"query": "[auto-failure:cron:Connection]"}, covered=("GiteaForgejoAdapter.search_issues.pagination",))
call_case("call:search-paginates", "search_issues",
[{"status": 200, "payload": [{"number": n, "title": f"issue {n}"} for n in range(50)]},
{"status": 200, "payload": [{"number": 100, "title": "[auto-failure:cron:Connection] cron"}]}],
{"query": "[auto-failure:cron:Connection]"},
covered=("GiteaForgejoAdapter.search_issues.pagination",))
call_case("call:search-filters-title-body", "search_issues",
[{"status": 200, "payload": [
{"number": 1, "title": "[auto-failure:cron:Connection] cron", "body": "x"},
{"number": 2, "title": "plain title", "body": "quoted [auto-failure:cron:Connection] in body"},
{"number": 3, "title": "mention of connection only", "body": "unrelated"}]}],
{"query": "[auto-failure:cron:Connection]"},
covered=("GiteaForgejoAdapter.search_issues.client-filter",))
call_case("call:search-no-hits", "search_issues",
[{"status": 200, "payload": [{"number": 1, "title": "unrelated"}]}],
{"query": "[auto-failure:cron:Connection]"})
call_case("call:search-exact-50-then-empty", "search_issues",
[{"status": 200, "payload": [{"number": n, "title": "noise"} for n in range(50)]},
{"status": 200, "payload": []}],
{"query": "[auto-failure:cron:Connection]"},
covered=("GiteaForgejoAdapter.search_issues.pagination",))
call_case("call:create-issue-retry-connect", "create_issue",
[{"exc": "connect"}, {"status": 200, "payload": {"number": 7}}],
{"title": "t", "body": "b"}, covered=("_request",))
call_case("call:get-issue-state-retry-5xx", "get_issue_state",
[{"status": 502}, {"status": 200, "payload": {"number": 9, "state": "open"}}],
{"issue_number": 9}, covered=("_request",))
call_case("call:get-issue-state-4xx", "get_issue_state",
[{"status": 404, "payload": {}}], {"issue_number": 404}, covered=("_request",))
# ── _request: every retry branch (noise vs signal) ──────────────────────────
request_case("request:get-ok", "GET", [{"status": 200, "payload": {"ok": True}}])
request_case("request:get-retries-connect", "GET",
[{"exc": "connect"}, {"exc": "connect"},
{"status": 200, "payload": {"ok": True}}])
request_case("request:get-connect-exhaust", "GET",
[{"exc": "connect"}, {"exc": "connect"}, {"exc": "connect"}])
request_case("request:get-4xx-no-retry", "GET", [{"status": 422}])
request_case("request:get-5xx-retry-then-ok", "GET",
[{"status": 502}, {"status": 503}, {"status": 200, "payload": {"n": 3}}])
request_case("request:get-5xx-exhaust", "GET", [{"status": 502}, {"status": 503}, {"status": 504}])
request_case("request:get-read-timeout-retries", "GET",
[{"exc": "read_timeout"}, {"status": 200, "payload": {"ok": True}}])
request_case("request:get-read-timeout-exhaust", "GET",
[{"exc": "read_timeout"}, {"exc": "read_timeout"}, {"exc": "read_timeout"}])
request_case("request:post-ok", "POST", [{"status": 200, "payload": {"a": 1}}],
json_body={"a": 1})
request_case("request:post-connect-retry", "POST",
[{"exc": "connect_timeout"}, {"status": 201, "payload": {"number": 7}}],
json_body={"a": 1})
request_case("request:post-connect-exhaust", "POST",
[{"exc": "connect"}, {"exc": "connect"}, {"exc": "connect"}],
json_body={"a": 1})
request_case("request:post-read-timeout-no-retry", "POST",
[{"exc": "read_timeout"}], json_body={"a": 1})
request_case("request:post-5xx-no-retry", "POST", [{"status": 502}], json_body={"a": 1})
request_case("request:post-4xx-no-retry", "POST", [{"status": 422}], json_body={"a": 1})
request_case("request:backoff-exponential", "GET",
[{"exc": "connect"}, {"exc": "connect"}, {"status": 200}])
request_case("request:custom-retries-backoff", "GET",
[{"exc": "connect"}, {"status": 200}], retries=1, backoff=0.25)
request_case("request:timeout-passthrough", "GET",
[{"status": 200}], timeout=17)
request_case("request:headers-json-capture", "POST",
[{"status": 200, "payload": {}}],
headers={"Authorization": "token t", "Content-Type": "application/json"},
json_body={"title": "t", "body": "b"})
request_case("request:lowercase-method", "post",
[{"status": 200, "payload": {"made": "it"}}], json_body={"k": "v"})
# ── freeze ───────────────────────────────────────────────────────────────────
EXPECTED = {
"create_forge_client", "_request",
"NullAdapter.create_issue", "NullAdapter.add_comment", "NullAdapter.get_issue_state",
"NullAdapter.list_open_issues", "NullAdapter.search_issues",
"GiteaForgejoAdapter.create_issue", "GiteaForgejoAdapter.add_comment",
"GiteaForgejoAdapter.get_issue_state", "GiteaForgejoAdapter.list_open_issues",
"GiteaForgejoAdapter.search_issues",
}
missing = EXPECTED - coverage
if missing:
sys.exit(f"COVERAGE GAP — public functions not exercised: {sorted(missing)}")
doc = {
"frozen_ts": "2026-10-12T02:17:44Z",
"oracle": "src/rune/forge_client.py",
"rows": rows,
}
OUT.parent.mkdir(parents=True, exist_ok=True)
OUT.write_text(json.dumps(doc, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
print(f"frozen {len(rows)} rows → {OUT}")
print("covered:", ", ".join(sorted(coverage)))