Add OpenStack request-body schemas and nested console PARAM sync.

This commit is contained in:
2026-07-18 08:47:16 +03:00
parent 6033967e6a
commit 2a8ce3a287
101 changed files with 150428 additions and 612 deletions
+38 -13
View File
@@ -9,19 +9,28 @@ from typing import Any
from _lib.validate import payload_nonempty
# Methods that normally return a JSON body on success (DELETE / 204 may be empty).
# Methods that normally return a JSON body on success (DELETE / 204 / HEAD may be empty).
_BODY_METHODS = frozenset({"GET", "POST", "PUT", "PATCH"})
_VERB_ORDER = ("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD")
def _expected_ops(packs: dict[str, Any], *, collections_only: bool) -> int:
def _expected_ops(packs: dict[str, Any], *, collections_only: bool) -> tuple[int, int, int]:
"""Return (expected_total, declared_pack_ops, synthetic_head_ops)."""
from app.openstack.surface_probe import count_declared_ops
if not collections_only:
return sum(len(p.operations) for p in packs.values())
total = 0
declared, heads = count_declared_ops(packs)
return declared + heads, declared, heads
declared = 0
heads = 0
for pack in packs.values():
for op in pack.operations:
if op.method == "GET" and "{" not in op.path:
total += 1
return total
declared += 1
heads += 1
return declared + heads, declared, heads
def _methods_breakdown(results: list[Any]) -> dict[str, int]:
@@ -30,16 +39,16 @@ def _methods_breakdown(results: list[Any]) -> dict[str, int]:
method = getattr(r, "method", None) or (r.get("method") if isinstance(r, dict) else None)
if method:
counts[str(method).upper()] += 1
return {m: counts.get(m, 0) for m in ("GET", "POST", "PUT", "PATCH", "DELETE")}
return {m: counts.get(m, 0) for m in _VERB_ORDER}
def _nonempty_from_lifecycle(report: Any) -> list[dict[str, Any]]:
"""Check succeeded lifecycle bodies (skip DELETE / 204 / 202 / no-body)."""
"""Check succeeded lifecycle bodies (skip DELETE / HEAD / 204 / 202 / no-body)."""
failures: list[dict[str, Any]] = []
for r in report.results:
if not r.succeeded:
continue
if r.method == "DELETE" or r.status in {202, 204}:
if r.method in {"DELETE", "HEAD"} or r.status in {202, 204}:
continue
if r.method not in _BODY_METHODS:
continue
@@ -74,6 +83,8 @@ def _nonempty_smoke_collections(
fill_path,
http_request,
issue_token,
microversion_headers,
service_base_url,
_seed_context,
)
@@ -96,8 +107,14 @@ def _nonempty_smoke_collections(
"account": project_id,
},
)
url = f"{host.rstrip('/')}{path}"
status, payload = http_request(op.method, url, token=token, service=pack.name)
url = f"{service_base_url(host, pack.name, port=pack.port)}{path}"
status, payload = http_request(
op.method,
url,
token=token,
service=pack.name,
headers=microversion_headers(pack, op) or None,
)
if status not in SUCCESS or status == 204:
continue
if not payload_nonempty(payload, collection_key=op.collection_key, method=op.method):
@@ -134,7 +151,7 @@ def probe_pack_operations(
)
packs = load_series_pack(series)
expected_ops = _expected_ops(packs, collections_only=collections_only)
expected_ops, declared_ops, head_ops = _expected_ops(packs, collections_only=collections_only)
methods = _methods_breakdown(report.results)
coverage_incomplete = len(report.results) != expected_ops
@@ -168,23 +185,31 @@ def probe_pack_operations(
"method": "*",
"path": "*",
"status": 0,
"detail": f"coverage_incomplete: total={len(report.results)} expected_ops={expected_ops}",
"detail": (
f"coverage_incomplete: total={len(report.results)} "
f"expected_ops={expected_ops} "
f"(declared={declared_ops} + HEAD={head_ops})"
),
"ok": False,
"nonempty": True,
}
)
critical = len(coverage_failures) + len(probe_failures) + len(nonempty_failures)
return {
"series": series,
"host": host,
"mode": report.mode,
"total": len(report.results),
"expected_ops": expected_ops,
"declared_ops": declared_ops,
"head_ops": head_ops,
"coverage_incomplete": coverage_incomplete,
"methods": methods,
"ok_count": len(report.results) - len(report.failures),
"fail_count": len(report.failures) + (1 if coverage_incomplete else 0),
"nonempty_fail_count": len(nonempty_failures),
"critical": critical,
"results": [
{
"service": r.service,
+6 -4
View File
@@ -12,7 +12,9 @@ SERIES_ORDER = ("yoga", "antelope", "caracal", "dalmatian")
def _methods_line(methods: dict[str, Any]) -> str:
return " ".join(f"{m}={methods.get(m, 0)}" for m in ("GET", "POST", "PUT", "PATCH", "DELETE"))
return " ".join(
f"{m}={methods.get(m, 0)}" for m in ("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD")
)
def render_html(summary: dict[str, Any], series_reports: list[dict[str, Any]]) -> str:
@@ -33,8 +35,8 @@ def render_html(summary: dict[str, Any], series_reports: list[dict[str, Any]]) -
</div>
<div class="stats">
<span class="ok">http ok={http.get("ok_count", 0)}</span>
<span class="fail">http fail={http.get("fail_count", 0)} nonempty_fail={http.get("nonempty_fail_count", 0)}</span>
<span>http total={http.get("total", 0)}/{http.get("expected_ops", "?")}</span>
<span class="fail">http fail={http.get("fail_count", 0)} nonempty_fail={http.get("nonempty_fail_count", 0)} critical={http.get("critical", 0)}</span>
<span>http total={http.get("total", 0)}/{http.get("expected_ops", "?")} (declared={http.get("declared_ops", "?")}+HEAD={http.get("head_ops", "?")})</span>
</div>
<div class="stats muted">
methods: {html.escape(_methods_line(http.get("methods") or {}))}
@@ -92,7 +94,7 @@ def render_html(summary: dict[str, Any], series_reports: list[dict[str, Any]]) -
<body>
<header>
<h1>Pulumi OpenStack API coverage</h1>
<p class="muted">Generated {html.escape(generated)} · pulumi_openstack primary + HTTP pack probe with non-empty checks</p>
<p class="muted">Generated {html.escape(generated)} · <strong>100% = HTTP contract matrix</strong> (pack ops + synthetic HEAD), not pulumi_openstack resource count. Layer B provider lifecycle is smoke only.</p>
</header>
<main>
<div class="summary">