Add OpenStack request-body schemas and nested console PARAM sync.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -233,10 +233,20 @@ def main() -> int:
|
||||
"total": 0,
|
||||
"expected_ops": 0,
|
||||
"coverage_incomplete": False,
|
||||
"methods": {"GET": 0, "POST": 0, "PUT": 0, "PATCH": 0, "DELETE": 0},
|
||||
"methods": {
|
||||
"GET": 0,
|
||||
"POST": 0,
|
||||
"PUT": 0,
|
||||
"PATCH": 0,
|
||||
"DELETE": 0,
|
||||
"HEAD": 0,
|
||||
},
|
||||
"ok_count": 0,
|
||||
"fail_count": 0,
|
||||
"nonempty_fail_count": 0,
|
||||
"critical": 0,
|
||||
"declared_ops": 0,
|
||||
"head_ops": 0,
|
||||
"results": [],
|
||||
"failures": [],
|
||||
}
|
||||
@@ -245,12 +255,15 @@ def main() -> int:
|
||||
http = run_http_coverage(series, collections_only=collections_only)
|
||||
methods = http.get("methods") or {}
|
||||
methods_s = " ".join(
|
||||
f"{m}={methods.get(m, 0)}" for m in ("GET", "POST", "PUT", "PATCH", "DELETE")
|
||||
f"{m}={methods.get(m, 0)}"
|
||||
for m in ("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD")
|
||||
)
|
||||
print(
|
||||
f"{series}: http ok={http.get('ok_count')} fail={http.get('fail_count')} "
|
||||
f"nonempty_fail={http.get('nonempty_fail_count')} "
|
||||
f"critical={http.get('critical')} "
|
||||
f"total={http.get('total')}/{http.get('expected_ops')} "
|
||||
f"(declared={http.get('declared_ops')}+HEAD={http.get('head_ops')}) "
|
||||
f"coverage_incomplete={http.get('coverage_incomplete')} "
|
||||
f"methods[{methods_s}]"
|
||||
)
|
||||
@@ -273,10 +286,16 @@ def main() -> int:
|
||||
int(r["http"].get("fail_count", 0)) + int(r["http"].get("nonempty_fail_count", 0))
|
||||
for r in series_reports
|
||||
),
|
||||
"http_critical": sum(int(r["http"].get("critical", 0)) for r in series_reports),
|
||||
"http_declared": sum(int(r["http"].get("declared_ops", 0)) for r in series_reports),
|
||||
"http_head": sum(int(r["http"].get("head_ops", 0)) for r in series_reports),
|
||||
"http_total": sum(int(r["http"].get("total", 0)) for r in series_reports),
|
||||
"http_expected": sum(int(r["http"].get("expected_ops", 0)) for r in series_reports),
|
||||
"coverage_incomplete": sum(
|
||||
1 for r in series_reports if r["http"].get("coverage_incomplete")
|
||||
),
|
||||
"collections_only": collections_only,
|
||||
"definition": "100% = HTTP contract matrix (pack ops + synthetic HEAD), not pulumi_openstack resource count",
|
||||
}
|
||||
html_path = write_html(REPORT_DIR, summary, series_reports)
|
||||
(REPORT_DIR / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
|
||||
@@ -285,7 +304,10 @@ def main() -> int:
|
||||
print(json.dumps(summary, indent=2))
|
||||
|
||||
failed = (
|
||||
summary["pulumi_fail"] > 0 or summary["http_fail"] > 0 or summary["coverage_incomplete"] > 0
|
||||
summary["pulumi_fail"] > 0
|
||||
or summary["http_fail"] > 0
|
||||
or summary["coverage_incomplete"] > 0
|
||||
or summary["http_critical"] > 0
|
||||
)
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user