verify-node.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. #!/usr/bin/env python3
  2. """Run one node verification case against the live SmartBotic services.
  3. Usage: scripts/verify-node.py tests/nodes/<case>.json
  4. Exit 0 if every assertion holds, 1 otherwise.
  5. """
  6. import json
  7. import sys
  8. import time
  9. import urllib.error
  10. import urllib.request
  11. BASE = "http://localhost:8090/api/v1"
  12. def call(method, path, token=None, body=None):
  13. data = json.dumps(body).encode() if body is not None else None
  14. req = urllib.request.Request(BASE + path, data=data, method=method)
  15. req.add_header("Content-Type", "application/json")
  16. if token:
  17. req.add_header("Authorization", "Bearer " + token)
  18. try:
  19. with urllib.request.urlopen(req, timeout=30) as res:
  20. raw = res.read()
  21. return json.loads(raw) if raw else {}
  22. except urllib.error.HTTPError as e:
  23. raise SystemExit(f"{method} {path} failed: {e.code} {e.read().decode()[:400]}")
  24. def login():
  25. return call("POST", "/auth/login", body={"username": "admin", "password": "admin"})["accessToken"]
  26. def subset_matches(expected, actual, path):
  27. """Every key in expected must be present and equal in actual. Returns list of failures."""
  28. fails = []
  29. if isinstance(expected, dict):
  30. if not isinstance(actual, dict):
  31. return [f"{path}: expected an object, got {type(actual).__name__}"]
  32. for key, want in expected.items():
  33. if key not in actual:
  34. fails.append(f"{path}.{key}: missing")
  35. else:
  36. fails += subset_matches(want, actual[key], f"{path}.{key}")
  37. elif isinstance(expected, list):
  38. if not isinstance(actual, list):
  39. return [f"{path}: expected a list, got {type(actual).__name__}"]
  40. if len(expected) != len(actual):
  41. fails.append(f"{path}: expected {len(expected)} items, got {len(actual)}")
  42. else:
  43. for i, want in enumerate(expected):
  44. fails += subset_matches(want, actual[i], f"{path}[{i}]")
  45. elif expected != actual:
  46. fails.append(f"{path}: expected {expected!r}, got {actual!r}")
  47. return fails
  48. def main():
  49. if len(sys.argv) != 2:
  50. raise SystemExit("usage: verify-node.py <case.json>")
  51. case = json.load(open(sys.argv[1]))
  52. token = login()
  53. call("POST", "/nodes/migrate", token, {"nodesPath": "./nodes"})
  54. created = call("POST", "/workflows", token, {
  55. "name": case["name"],
  56. "nodes": case["nodes"],
  57. "connections": case["connections"],
  58. "settings": {},
  59. })
  60. workflow_id = created.get("id") or created.get("_id")
  61. if not workflow_id:
  62. raise SystemExit(f"no workflow id in create response: {json.dumps(created)[:300]}")
  63. try:
  64. started = call("POST", f"/workflows/{workflow_id}/execute", token, {})
  65. execution_id = started["executionId"]
  66. execution = None
  67. for _ in range(60):
  68. time.sleep(0.5)
  69. execution = call("GET", f"/executions/{execution_id}", token)
  70. if execution.get("status") in ("completed", "failed", "cancelled"):
  71. break
  72. else:
  73. raise SystemExit(f"execution {execution_id} did not finish in 30s")
  74. by_id = {n["nodeId"]: n for n in execution.get("nodeExecutions", [])}
  75. failures = []
  76. for node_id, want in case.get("expect", {}).items():
  77. got = by_id.get(node_id)
  78. if got is None:
  79. failures.append(f"{node_id}: did not run")
  80. continue
  81. if "status" in want and got.get("status") != want["status"]:
  82. failures.append(
  83. f"{node_id}: status {got.get('status')!r}, expected {want['status']!r}"
  84. + (f" (error: {got.get('error')})" if got.get("error") else "")
  85. )
  86. if "output" in want:
  87. failures += subset_matches(want["output"], got.get("output"), node_id)
  88. for node_id in case.get("expectMissing", []):
  89. got = by_id.get(node_id)
  90. if got is not None and got.get("status") == "completed":
  91. failures.append(f"{node_id}: ran, but should not have")
  92. print(f"case: {case['name']} execution: {execution_id} status: {execution.get('status')}")
  93. for node_id, node in sorted(by_id.items()):
  94. print(f" {node_id:24} {node.get('status'):10} {json.dumps(node.get('output'))[:120]}")
  95. if failures:
  96. print("\nFAIL")
  97. for f in failures:
  98. print(" " + f)
  99. return 1
  100. print("\nPASS")
  101. return 0
  102. finally:
  103. call("DELETE", f"/workflows/{workflow_id}", token)
  104. if __name__ == "__main__":
  105. sys.exit(main())