Skip to content

Commit d2c1493

Browse files
committed
test(smoke): add GCP smoke run + Cloud Build config
1 parent b22b18c commit d2c1493

2 files changed

Lines changed: 323 additions & 0 deletions

File tree

tests/smoke/cloudbuild.yaml

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Cloud Build pipeline to build app image, deploy to Cloud Run, generate Workflows YAML, deploy Workflows,
2+
# and run a smoke execution.
3+
4+
options:
5+
logging: CLOUD_LOGGING_ONLY
6+
substitutionOption: ALLOW_LOOSE
7+
substitutions:
8+
_REGION: us-central1
9+
_SERVICE_NAME: fastapi-cloudflow-example
10+
_ORG: flamingo-run
11+
12+
steps:
13+
- name: gcr.io/cloud-builders/docker
14+
id: build-image
15+
args: ["build", "-t", "${_REGION}-docker.pkg.dev/$PROJECT_ID/${_ORG}/${_SERVICE_NAME}:$SHORT_SHA", "-f", "examples/app/Dockerfile", "."]
16+
17+
- name: gcr.io/cloud-builders/docker
18+
id: push-image
19+
args: ["push", "${_REGION}-docker.pkg.dev/$PROJECT_ID/${_ORG}/${_SERVICE_NAME}:$SHORT_SHA"]
20+
21+
- name: gcr.io/google.com/cloudsdktool/cloud-sdk
22+
id: deploy-run
23+
entrypoint: bash
24+
args:
25+
- -c
26+
- |
27+
gcloud run deploy ${_SERVICE_NAME} \
28+
--image ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_ORG}/${_SERVICE_NAME}:$SHORT_SHA \
29+
--region ${_REGION} --no-allow-unauthenticated --quiet
30+
31+
- name: ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_ORG}/${_SERVICE_NAME}:$SHORT_SHA
32+
id: build-workflows
33+
entrypoint: bash
34+
args:
35+
- -c
36+
- |
37+
set -euo pipefail
38+
mkdir -p /workspace/build/yaml
39+
cd /app
40+
PYTHONPATH=/app fastapi-cloudflow build \
41+
--app-spec main:app \
42+
--flows-path flows \
43+
--out /workspace/build/yaml
44+
echo "Listing generated YAMLs in /workspace/build/yaml:"
45+
ls -la /workspace/build/yaml || true
46+
shopt -s nullglob
47+
files=(/workspace/build/yaml/*.yaml)
48+
if [ ${#files[@]} -eq 0 ]; then
49+
echo "ERROR: No YAML files generated by fastapi-cloudflow build"
50+
exit 1
51+
fi
52+
53+
- name: gcr.io/google.com/cloudsdktool/cloud-sdk
54+
id: deploy-workflows
55+
entrypoint: bash
56+
args:
57+
- -c
58+
- |
59+
set -euo pipefail
60+
BASE_URL=$(gcloud run services describe "${_SERVICE_NAME}" --region "${_REGION}" --platform=managed --project "$PROJECT_ID" --format='value(status.url)')
61+
echo "Resolved BASE_URL=$$BASE_URL"
62+
shopt -s nullglob
63+
files=(/workspace/build/yaml/*.yaml)
64+
if [ ${#files[@]} -eq 0 ]; then
65+
echo "ERROR: No YAML files found to deploy at /workspace/build/yaml"
66+
ls -la /workspace/build || true
67+
ls -la /workspace/build/yaml || true
68+
exit 1
69+
fi
70+
for f in "${files[@]}"; do
71+
name=$(basename "$f" .yaml)
72+
echo "Deploying workflow $$name from $$f"
73+
gcloud workflows deploy "$$name" \
74+
--location "${_REGION}" \
75+
--source="$$f" \
76+
--set-env-vars="BASE_URL=$${BASE_URL},ECHO_URL=https://httpbin.org/anything,PSP_URL=$${BASE_URL},IDP_URL=$${BASE_URL}" \
77+
--quiet
78+
done
79+
80+
- name: gcr.io/google.com/cloudsdktool/cloud-sdk
81+
id: run-smoke
82+
entrypoint: bash
83+
env:
84+
- GOOGLE_CLOUD_PROJECT=$PROJECT_ID
85+
args:
86+
- -c
87+
- |
88+
python3 tests/smoke/run_smoke.py --region ${_REGION}
89+
90+
- name: gcr.io/google.com/cloudsdktool/cloud-sdk
91+
id: cleanup
92+
entrypoint: bash
93+
args:
94+
- -c
95+
- |
96+
echo "Cleaning up resources..."
97+
# Clean up Cloud Run service
98+
gcloud run services delete ${_SERVICE_NAME} --region ${_REGION} --quiet || true
99+
100+
# Clean up deployed workflows based on generated YAMLs
101+
shopt -s nullglob
102+
files=(/workspace/build/yaml/*.yaml)
103+
if [ ${#files[@]} -eq 0 ]; then
104+
echo "No YAML files found for cleanup" || true
105+
else
106+
for f in "${files[@]}"; do
107+
name=$(basename "$f" .yaml)
108+
echo "Deleting workflow $$name"
109+
gcloud workflows delete "$$name" --location ${_REGION} --quiet || true
110+
done
111+
fi
112+
113+
# Clean up Artifact Registry image
114+
gcloud artifacts docker images delete ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_ORG}/${_SERVICE_NAME}:$SHORT_SHA --delete-tags --quiet || true
115+
116+
echo "Cleanup completed"
117+
118+
images:
119+
- ${_REGION}-docker.pkg.dev/$PROJECT_ID/${_ORG}/${_SERVICE_NAME}:$SHORT_SHA

tests/smoke/run_smoke.py

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
#!/usr/bin/env python3
2+
from __future__ import annotations
3+
4+
import argparse
5+
import json
6+
import os
7+
import shlex
8+
import subprocess
9+
import sys
10+
import time
11+
from typing import Any
12+
13+
14+
def run_command(args: list[str]) -> str:
15+
# Avoid shell quoting issues by passing list args
16+
completed = subprocess.run(args, capture_output=True, text=True)
17+
if completed.returncode != 0:
18+
sys.stderr.write(f"Command failed ({completed.returncode}): {' '.join(shlex.quote(a) for a in args)}\n")
19+
sys.stderr.write(completed.stderr)
20+
raise SystemExit(1)
21+
return completed.stdout.strip()
22+
23+
24+
def resolve_project() -> str:
25+
project = os.environ.get("GOOGLE_CLOUD_PROJECT") or os.environ.get("PROJECT_ID")
26+
if project:
27+
return project
28+
try:
29+
return run_command(["gcloud", "config", "get-value", "project"]) or ""
30+
except SystemExit:
31+
return ""
32+
33+
34+
def run_workflow(
35+
workflow_name: str, region: str, project: str, payload: dict[str, Any], timeout_sec: int = 180
36+
) -> dict[str, Any]:
37+
data = json.dumps({"payload": payload})
38+
exec_name = run_command(
39+
[
40+
"gcloud",
41+
"workflows",
42+
"run",
43+
workflow_name,
44+
"--location",
45+
region,
46+
"--project",
47+
project,
48+
"--data",
49+
data,
50+
"--format=value(name)",
51+
]
52+
)
53+
54+
# Poll describe for state to avoid gcloud version mismatches
55+
deadline = time.time() + timeout_sec
56+
describe_args = [
57+
"gcloud",
58+
"workflows",
59+
"executions",
60+
"describe",
61+
exec_name,
62+
"--location",
63+
region,
64+
"--project",
65+
project,
66+
"--format=json",
67+
]
68+
while True:
69+
out = run_command(describe_args)
70+
desc = json.loads(out)
71+
state = desc.get("state")
72+
if state == "SUCCEEDED":
73+
result_raw = desc.get("result", "")
74+
if not result_raw:
75+
sys.stderr.write("Execution SUCCEEDED but result is empty\n")
76+
sys.stderr.write(json.dumps(desc, indent=2) + "\n")
77+
raise SystemExit(1)
78+
try:
79+
return json.loads(result_raw)
80+
except json.JSONDecodeError as err:
81+
sys.stderr.write("Result is not valid JSON:\n" + result_raw + "\n")
82+
sys.stderr.write(json.dumps(desc, indent=2) + "\n")
83+
raise SystemExit(1) from err
84+
if state in {"FAILED", "ERROR", "CANCELLED"}:
85+
sys.stderr.write(f"Execution ended with state={state}\n")
86+
sys.stderr.write(json.dumps(desc, indent=2) + "\n")
87+
raise SystemExit(1)
88+
if time.time() > deadline:
89+
sys.stderr.write("Timed out waiting for execution to finish\n")
90+
sys.stderr.write(json.dumps(desc, indent=2) + "\n")
91+
raise SystemExit(1)
92+
time.sleep(2)
93+
94+
95+
def assert_order_flow(result: dict[str, Any]) -> None:
96+
assert result.get("status") == "approved", result
97+
order_id = result.get("order_id", "")
98+
assert isinstance(order_id, str) and order_id.startswith("o-"), result
99+
100+
101+
def assert_payment_flow(result: dict[str, Any]) -> None:
102+
assert result.get("ok") is True, result
103+
txn_id = result.get("txn_id")
104+
assert isinstance(txn_id, str) and len(txn_id) > 0, result
105+
106+
107+
def assert_user_signup(result: dict[str, Any], email: str) -> None:
108+
assert result.get("user_id"), result
109+
assert result.get("email") == email, result
110+
111+
112+
def main() -> None:
113+
parser = argparse.ArgumentParser(description="Run smoke tests against deployed Workflows")
114+
parser.add_argument("--region", default="us-central1")
115+
parser.add_argument("--project", default=None)
116+
parser.add_argument(
117+
"--only",
118+
choices=[
119+
"order-flow",
120+
"payment-flow",
121+
"user-signup",
122+
"echo-name-flow",
123+
"post-story-flow",
124+
"joke-flow",
125+
"convert-flow",
126+
],
127+
action="append",
128+
)
129+
args = parser.parse_args()
130+
131+
project = args.project or resolve_project()
132+
if not project:
133+
print("Project not set. Set GOOGLE_CLOUD_PROJECT/PROJECT_ID or pass --project.", file=sys.stderr)
134+
sys.exit(1)
135+
136+
include = set(
137+
args.only
138+
or [
139+
"order-flow",
140+
"payment-flow",
141+
"user-signup",
142+
"echo-name-flow",
143+
"post-story-flow",
144+
"joke-flow",
145+
]
146+
)
147+
148+
print(f"Running smoke tests in project={project} region={args.region}")
149+
150+
if "order-flow" in include:
151+
print("- order-flow case 1")
152+
res1 = run_workflow("order-flow", args.region, project, {"account_id": 1, "sku": "abc", "qty": 1})
153+
assert_order_flow(res1)
154+
print(" OK", res1)
155+
print("- order-flow case 2")
156+
res2 = run_workflow("order-flow", args.region, project, {"account_id": 2, "sku": "xyz", "qty": 3})
157+
assert_order_flow(res2)
158+
print(" OK", res2)
159+
160+
if "payment-flow" in include:
161+
print("- payment-flow")
162+
pres = run_workflow("payment-flow", args.region, project, {"total": 12.34, "currency": "USD"})
163+
assert_payment_flow(pres)
164+
print(" OK", pres)
165+
166+
if "user-signup" in include:
167+
print("- user-signup")
168+
email = "user@example.com"
169+
ures = run_workflow("user-signup", args.region, project, {"email": email, "password": "hunter2asdf"})
170+
assert_user_signup(ures, email)
171+
print(" OK", ures)
172+
173+
if "echo-name-flow" in include:
174+
print("- echo-name-flow")
175+
eres = run_workflow("echo-name-flow", args.region, project, {"name": "Developer"})
176+
# Final step returns a shout summary
177+
assert isinstance(eres.get("length"), int) and isinstance(eres.get("name_upper"), str), eres
178+
print(" OK", eres)
179+
180+
if "post-story-flow" in include:
181+
print("- post-story-flow")
182+
jres = run_workflow(
183+
"post-story-flow",
184+
args.region,
185+
project,
186+
{"topic": "hello", "mood": "curious", "author_id": 1},
187+
)
188+
assert isinstance(jres.get("id"), int) and "slug" in jres and "short_title" in jres, jres
189+
print(" OK", jres)
190+
191+
if "joke-flow" in include:
192+
print("- joke-flow")
193+
jf = run_workflow("joke-flow", args.region, project, {})
194+
# From rated joke, ensure shape
195+
assert "setup" in jf and "punch" in jf and isinstance(jf.get("rating"), int), jf
196+
print(" OK", jf)
197+
198+
# convert-flow disabled until branch/merge is supported in core
199+
200+
print("All smoke tests passed.")
201+
202+
203+
if __name__ == "__main__":
204+
main()

0 commit comments

Comments
 (0)