#!/usr/bin/env python3 """Bounded local acceptance of API-generated projects, on both Omni transports. No remote hosts, cloud credentials or database are used. Child servers and temporary modules are always cleaned up. Run from any directory with Python 3. """ import hashlib import http.client import json import os from pathlib import Path import signal import socket import subprocess import tempfile import time ROOT = Path(__file__).resolve().parents[1] REPORT = ROOT / "performance/apiGenerationLocal.json" ENV = {k: v for k, v in os.environ.items() if not k.startswith("GO_OMNI_")} ENV.update(GOWORK="off", GOFLAGS="") def run(args, cwd=ROOT, ok=True): result = subprocess.run([str(x) for x in args], cwd=cwd, env=ENV, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=90) if ok and result.returncode: raise AssertionError(f"{args}:\n{result.stdout}") if not ok and not result.returncode: raise AssertionError(f"command unexpectedly succeeded: {args}") return result.stdout def request(port, method, path, body=None, headers=None): conn = http.client.HTTPConnection("127.0.0.1", port, timeout=3) try: if body is not None and len(body) > (1 << 20): # Probe early Content-Length rejection without racing a 1 MiB # client send against the server's intentional connection close. conn.putrequest(method, path) for key, value in (headers or {}).items(): conn.putheader(key, value) conn.putheader("Content-Length", str(len(body))) conn.endheaders() else: conn.request(method, path, body=body, headers=headers or {}) response = conn.getresponse() return response.status, dict(response.getheaders()), response.read() finally: conn.close() def write(path, text): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text) def exercise(binary, project, transport, starter): with socket.socket() as sock: sock.bind(("127.0.0.1", 0)) port = sock.getsockname()[1] environment = dict(ENV, GO_OMNI_HTTP_ADDR=f"127.0.0.1:{port}", GO_OMNI_TRANSPORT=transport, GO_OMNI_LOG_OUTPUT="stderr", GO_OMNI_LOG_FORMAT="json") checks = [] log_path = project / f"{transport}.log" with log_path.open("w+") as log: process = subprocess.Popen([str(binary)], cwd=project, env=environment, stdout=log, stderr=log) try: deadline = time.monotonic() + 10 while True: if process.poll() is not None: raise AssertionError(log_path.read_text()) try: if request(port, "GET", "/health/ready")[0] == 200: break except OSError: pass if time.monotonic() > deadline: raise AssertionError("server startup timed out") time.sleep(0.05) checks.append("readiness") status, headers, document = request(port, "GET", "/openapi.json") assert status == 200 and document == (project / "docs/openapi.json").read_bytes() checks.append("servedOpenApiMatchesFile") if starter: status, headers, data = request(port, "GET", "/api/greet/Alice") assert status == 200 and json.loads(data)["data"]["message"] == "Hello Alice" assert headers.get("X-Request-Id") or headers.get("X-Request-ID") checks.extend(["greeting", "requestId"]) else: body = json.dumps({"name": "Alice", "active": False, "age": 0}) base = {"Content-Type": "application/json", "X-Request-ID": "api-acceptance-42"} for method, path, payload, expected in [ ("PUT", "/api/v1/users/7?notify=true", body, 200), ("PUT", "/api/v1/users/7", '{"name":"Alice"}', 400), ("PUT", "/api/v1/users/7?notify=bad", body, 400), ("PUT", "/api/v1/users/7?notify=true¬ify=false", body, 400), ("PUT", "/api/v1/users/7", body[:-1] + ',"extra":1}', 400), ("PUT", "/api/v1/users/7", body + "{}", 400), ("PUT", "/api/v1/users/7", '{"name":"A","active":false,"age":151}', 400), ("PUT", "/api/v1/users/0", body, 400), ("PUT", "/api/v1/users/7", " " * ((1 << 20) + 1), 413), ("GET", "/api/v1/users?page=2&pageSize=10&tag=a&tag=b", None, 200), ("GET", "/api/v1/users?page=0", None, 400), ("DELETE", "/admin/v1/users/7", None, 401), ("POST", "/api/v1/newUsers", body, 201), ("POST", "/api/v1/acceptedUsers", body, 202), ]: status, headers, data = request(port, method, path, payload, base) assert status == expected, (method, path, status, data) if path.startswith("/api/v1/users/7?") and status == 200: assert json.loads(data)["data"] == {"userId": 7, "name": "Alice", "active": False} assert json.loads(data)["requestId"] == "api-acceptance-42" if path.startswith("/api/v1/users?page=2"): assert json.loads(data)["data"]["page"] == 2 checks.extend(["mixedSources", "zeroAndFalse", "pathRange", "defaultAndQuery", "requiredFields", "unknownFields", "duplicateScalar", "trailingJson", "rangeValidation", "bodyLimit413", "auth401", "status201", "status202"]) assert request(port, "PUT", "/api/v1/users/7", body, {"Content-Type": "text/plain"})[0] == 415 status, headers, data = request(port, "DELETE", "/admin/v1/users/7", headers={"Authorization": "Bearer test-only"}) assert status == 204 and data == b"" and headers.get("X-Audit") == "yes" checks.extend(["contentType415", "middlewareComposition", "noContent204"]) process.send_signal(signal.SIGTERM) assert process.wait(timeout=12) == 0 checks.append("gracefulShutdown") try: request(port, "GET", "/health/live") raise AssertionError("listener survived shutdown") except OSError: checks.append("listenerReleased") finally: if process.poll() is None: process.kill() process.wait(timeout=5) if not starter: logs = [json.loads(line) for line in log_path.read_text().splitlines() if line.startswith("{")] assert any(row.get("msg") == "user.updated" and row.get("requestId") == "api-acceptance-42" for row in logs) checks.append("businessLogCorrelation") return {"project": "starter" if starter else "contract", "transport": transport, "checks": checks, "passed": True} def main(): started = time.monotonic() results = [] with tempfile.TemporaryDirectory(prefix="goOmniApiAcceptance") as temp: temp = Path(temp) cli = temp / "go-omni" run(["go", "build", "-o", cli, "./cmd/go-omni"]) for starter in (True, False): project = temp / ("starter" if starter else "contract") common = ["--module", "example.com/apiAcceptance", "--framework-dir", ROOT] if starter: run([cli, "api", "new", *common, project]) entry = project / "api/service.api" else: run([cli, "new", "--api", ROOT / "examples/contracts/user.api", *common, project]) entry = project / "api/user.api" business = '''package user import("context";"example.com/apiAcceptance/internal/types") func(s *Service) UpdateUser(ctx context.Context,req *types.UpdateUserReq)(*types.UserResp,error){ s.deps.Logger.InfoContext(ctx,"user.updated","userId",req.UserID) return &types.UserResp{UserID:req.UserID,Name:req.Name,Active:req.Active},nil } ''' write(project / "internal/service/user/updateUser.go", business) write(project / "internal/service/user/listUsers.go", '''package user import("context";"example.com/apiAcceptance/internal/types") func(s *Service) ListUsers(ctx context.Context,req *types.ListUsersReq)(*types.ListUsersResp,error){return &types.ListUsersResp{Items:[]types.UserResp{},Page:req.Page,Total:0},nil} ''') write(project / "internal/service/admin/user/deleteUser.go", '''package user import("context";"example.com/apiAcceptance/internal/types") func(s *Service) DeleteUser(ctx context.Context,req *types.DeleteUserReq)error{return nil} ''') write(project / "internal/middleware/auth.go", '''package middleware import("git.yuntuo.hk/k2safe/go-omni/httpx";apierrors "git.yuntuo.hk/k2safe/go-omni/errors") func Auth(next httpx.Handler)httpx.Handler{return func(c *httpx.Context)error{if c.Header("Authorization")!="Bearer test-only"{return apierrors.Unauthorized("UNAUTHORIZED","authentication required")};return next(c)}} ''') write(project / "internal/middleware/audit.go", '''package middleware import("git.yuntuo.hk/k2safe/go-omni/httpx") func Audit(next httpx.Handler)httpx.Handler{return func(c *httpx.Context)error{c.SetResponseHeader("X-Audit","yes");return next(c)}} ''') source = entry.read_text().replace(" UpdateUserReq {", ' CreateUserReq {\n Name string `json:"name"`\n Active bool `json:"active"`\n Age *int `json:"age,optional"`\n }\n UpdateUserReq {') source += ''' @server ( group: user prefix: /api/v1 ) service user-api { @handler createUser @status 201 post /newUsers (CreateUserReq) returns (UserResp) @handler acceptUser @status 202 post /acceptedUsers (CreateUserReq) returns (UserResp) } ''' entry.write_text(source) run([cli, "api", "generate", "--api", entry, "--dir", project, "--check"], ok=False) run([cli, "api", "generate", "--api", entry, "--dir", project]) assert (project / "internal/service/user/updateUser.go").read_text() == business for operation in ("CreateUser", "AcceptUser"): name = operation[0].lower() + operation[1:] write(project / f"internal/service/user/{name}.go", f'''package user import("context";"example.com/apiAcceptance/internal/types") func(s *Service) {operation}(ctx context.Context,req *types.CreateUserReq)(*types.UserResp,error){{return &types.UserResp{{UserID:8,Name:req.Name,Active:req.Active}},nil}} ''') run([cli, "api", "validate", "--api", entry]) run([cli, "api", "generate", "--api", entry, "--dir", project, "--check"]) binary = project / "server" run(["go", "build", "-mod=mod", "-o", binary, "./cmd/server"], cwd=project) for transport in ("nethttp", "native"): result = exercise(binary, project, transport, starter) results.append(result) print(f"PASS {result['project']}/{transport}: {len(result['checks'])} checks", flush=True) files = [] for folder in ("cmd/go-omni/apigen", "contract", "openapi", "examples/contracts"): files.extend(p for p in (ROOT / folder).rglob("*") if p.is_file()) files.extend(ROOT / p for p in ("cmd/go-omni/api.go", "cmd/go-omni/main.go", "scripts/verifyAPI.py", "transport/native/server.go", "transport/native/earlyReject_test.go")) report = {"passed": True, "durationSeconds": round(time.monotonic() - started, 2), "cases": results, "sourceSha256": {str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(files)}} REPORT.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n") print(f"Saved {REPORT}", flush=True) if __name__ == "__main__": main()