347 lines
15 KiB
Python
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)))
|