Files
openstack-api-simulator/tools/scan_empty_collections.py
T
inecs 6033967e6a Initial commit: stateful OpenStack API laboratory simulator.
Ship Keystone auth, multi-service handlers (Yoga→Dalmatian), Compose/Helm
packaging, API contract packs, and pytest/Pulumi coverage labs.
2026-07-18 04:26:48 +03:00

102 lines
3.3 KiB
Python

#!/usr/bin/env python3
"""Scan GET collection endpoints for empty list payloads across all series."""
from __future__ import annotations
import argparse
import json
import sys
from collections import defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from app.openstack.contract_loader import list_series, load_series_pack # noqa: E402
from app.openstack.surface_probe import ( # noqa: E402
activate_series,
fill_path,
http_request,
issue_token,
)
def _is_empty_list_payload(body: object) -> tuple[bool, str | None]:
if not isinstance(body, dict):
return False, None
if body.get("data") == []:
return True, "data"
for key, value in body.items():
if key in {"links", "metadata", "versions", "version", "id", "status"}:
continue
if isinstance(value, list) and len(value) == 0:
return True, key
return False, None
def _is_top_level_collection(op) -> bool: # noqa: ANN001
if op.method != "GET":
return False
if op.kind in {"collection", "detail"}:
return "{" not in op.path or op.path.rstrip("/").endswith("/detail")
if op.kind == "custom" and "{" not in op.path:
return True
return False
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--host", default="http://api-gateway:5000")
parser.add_argument("--series", action="append", default=[])
args = parser.parse_args()
host = args.host.rstrip("/")
if args.series:
series_list = args.series
else:
series_list = [str(item["series"]) for item in list_series()]
token, _ = issue_token(host, user="admin", project="admin")
empties: list[tuple[str, str, str, str, str | None, int, str | None]] = []
checked = 0
for series in series_list:
activate_series(host, series)
packs = load_series_pack(series)
for name, pack in sorted(packs.items()):
for op in pack.operations:
if not _is_top_level_collection(op):
continue
path = fill_path(op.path)
status, body = http_request("GET", f"{host}{path}", token=token, service=name)
checked += 1
empty, key = _is_empty_list_payload(body)
if empty:
empties.append(
(
series,
name,
op.path,
op.resource_type,
op.collection_key,
status,
key,
)
)
by_path: dict[tuple[str, str, str, str | None], list[str]] = defaultdict(list)
for series, svc, path, rtype, ckey, _status, _key in empties:
by_path[(svc, rtype, path, ckey)].append(series)
print(json.dumps({"checked": checked, "empty": len(empties), "unique": len(by_path)}, indent=2))
print("\n=== EMPTY COLLECTIONS ===")
for (svc, rtype, path, ckey), serieses in sorted(by_path.items()):
print(
f"{svc:12} {rtype:28} key={str(ckey):24} {path} "
f"series={','.join(sorted(set(serieses)))}"
)
return 0 if not by_path else 1
if __name__ == "__main__":
raise SystemExit(main())