prefect server in zig
at main 485 lines 16 kB view raw
1#!/usr/bin/env bash 2set -euo pipefail 3 4# Test harness for database backends 5# Usage: 6# ./scripts/test-db-backends # test SQLite only (default) 7# ./scripts/test-db-backends sqlite # test SQLite 8# ./scripts/test-db-backends postgres # test PostgreSQL (starts Docker) 9# ./scripts/test-db-backends all # test both 10 11SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" 12PROJECT_DIR="$(dirname "$SCRIPT_DIR")" 13 14RED='\033[0;31m' 15GREEN='\033[0;32m' 16YELLOW='\033[1;33m' 17BLUE='\033[0;34m' 18NC='\033[0m' 19 20log_info() { echo -e "${GREEN}[INFO]${NC} $*"; } 21log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } 22log_error() { echo -e "${RED}[ERROR]${NC} $*"; } 23log_step() { echo -e "${BLUE}[STEP]${NC} $*"; } 24 25BACKEND="${1:-sqlite}" 26TEST_DB_PATH="/tmp/prefect-test-$$.db" 27POSTGRES_CONTAINER="prefect-test-postgres" 28POSTGRES_PORT=5433 # use non-standard port to avoid conflicts 29POSTGRES_USER="prefect" 30POSTGRES_PASSWORD="prefect" 31POSTGRES_DB="prefect_test" 32POSTGRES_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:${POSTGRES_PORT}/${POSTGRES_DB}" 33SERVER_PID="" 34 35cleanup() { 36 # Kill server if running 37 if [[ -n "$SERVER_PID" ]] && kill -0 "$SERVER_PID" 2>/dev/null; then 38 log_info "Stopping server (PID $SERVER_PID)..." 39 kill "$SERVER_PID" 2>/dev/null || true 40 wait "$SERVER_PID" 2>/dev/null || true 41 fi 42 43 # Clean up test database 44 rm -f "$TEST_DB_PATH" 2>/dev/null || true 45} 46trap cleanup EXIT 47 48start_postgres_docker() { 49 log_step "Setting up PostgreSQL via Docker..." 50 51 # Check if Docker is available 52 if ! command -v docker &> /dev/null; then 53 log_error "Docker not found. Install Docker to test PostgreSQL backend." 54 return 1 55 fi 56 57 # Stop any existing container 58 docker rm -f "$POSTGRES_CONTAINER" 2>/dev/null || true 59 60 # Start PostgreSQL container 61 log_info "Starting PostgreSQL container on port $POSTGRES_PORT..." 62 if ! docker run -d \ 63 --name "$POSTGRES_CONTAINER" \ 64 -e POSTGRES_USER="$POSTGRES_USER" \ 65 -e POSTGRES_PASSWORD="$POSTGRES_PASSWORD" \ 66 -e POSTGRES_DB="$POSTGRES_DB" \ 67 -p "${POSTGRES_PORT}:5432" \ 68 postgres:16-alpine \ 69 > /dev/null 2>&1; then 70 log_error "Failed to start PostgreSQL container. Is Docker running?" 71 return 1 72 fi 73 74 # Wait for PostgreSQL to be ready 75 log_info "Waiting for PostgreSQL to be ready..." 76 local max_attempts=30 77 local attempt=0 78 while [[ $attempt -lt $max_attempts ]]; do 79 if docker exec "$POSTGRES_CONTAINER" pg_isready -U "$POSTGRES_USER" &> /dev/null; then 80 log_info "PostgreSQL is ready" 81 return 0 82 fi 83 ((attempt++)) 84 sleep 1 85 done 86 87 log_error "PostgreSQL failed to start within ${max_attempts}s" 88 return 1 89} 90 91stop_postgres_docker() { 92 log_info "Stopping PostgreSQL container..." 93 docker rm -f "$POSTGRES_CONTAINER" 2>/dev/null || true 94} 95 96# Sanity check: verify data in SQLite 97sanity_check_sqlite() { 98 local db_path="$1" 99 local flow_id="$2" 100 local flow_run_id="$3" 101 102 log_step "Sanity checks: verifying SQLite data..." 103 104 # Check flow exists 105 local flow_count 106 flow_count=$(sqlite3 "$db_path" "SELECT COUNT(*) FROM flow WHERE id='$flow_id'") 107 if [[ "$flow_count" != "1" ]]; then 108 log_error "Flow not found in database (expected 1, got $flow_count)" 109 return 1 110 fi 111 log_info " flow exists: yes" 112 113 # Check flow run exists 114 local flow_run_count 115 flow_run_count=$(sqlite3 "$db_path" "SELECT COUNT(*) FROM flow_run WHERE id='$flow_run_id'") 116 if [[ "$flow_run_count" != "1" ]]; then 117 log_error "Flow run not found in database (expected 1, got $flow_run_count)" 118 return 1 119 fi 120 log_info " flow_run exists: yes" 121 122 # Check state history 123 local state_count 124 state_count=$(sqlite3 "$db_path" "SELECT COUNT(*) FROM flow_run_state WHERE flow_run_id='$flow_run_id'") 125 if [[ "$state_count" -lt 1 ]]; then 126 log_error "No state history for flow run (expected >= 1, got $state_count)" 127 return 1 128 fi 129 log_info " state history: $state_count states" 130 131 # Check final state 132 local final_state 133 final_state=$(sqlite3 "$db_path" "SELECT type FROM flow_run_state WHERE flow_run_id='$flow_run_id' ORDER BY timestamp DESC LIMIT 1") 134 log_info " final state: $final_state" 135 136 # Count total rows in key tables 137 local total_flows total_runs total_states 138 total_flows=$(sqlite3 "$db_path" "SELECT COUNT(*) FROM flow") 139 total_runs=$(sqlite3 "$db_path" "SELECT COUNT(*) FROM flow_run") 140 total_states=$(sqlite3 "$db_path" "SELECT COUNT(*) FROM flow_run_state") 141 log_info " table counts: flows=$total_flows, runs=$total_runs, states=$total_states" 142 143 return 0 144} 145 146# Sanity check: verify data in PostgreSQL 147sanity_check_postgres() { 148 local flow_id="$1" 149 local flow_run_id="$2" 150 151 log_step "Sanity checks: verifying PostgreSQL data..." 152 153 # Check flow exists 154 local flow_count 155 flow_count=$(docker exec "$POSTGRES_CONTAINER" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -t -c \ 156 "SELECT COUNT(*) FROM flow WHERE id='$flow_id'" 2>/dev/null | tr -d ' ') 157 if [[ "$flow_count" != "1" ]]; then 158 log_error "Flow not found in database (expected 1, got $flow_count)" 159 return 1 160 fi 161 log_info " flow exists: yes" 162 163 # Check flow run exists 164 local flow_run_count 165 flow_run_count=$(docker exec "$POSTGRES_CONTAINER" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -t -c \ 166 "SELECT COUNT(*) FROM flow_run WHERE id='$flow_run_id'" 2>/dev/null | tr -d ' ') 167 if [[ "$flow_run_count" != "1" ]]; then 168 log_error "Flow run not found in database (expected 1, got $flow_run_count)" 169 return 1 170 fi 171 log_info " flow_run exists: yes" 172 173 # Check state history 174 local state_count 175 state_count=$(docker exec "$POSTGRES_CONTAINER" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -t -c \ 176 "SELECT COUNT(*) FROM flow_run_state WHERE flow_run_id='$flow_run_id'" 2>/dev/null | tr -d ' ') 177 if [[ "$state_count" -lt 1 ]]; then 178 log_error "No state history for flow run (expected >= 1, got $state_count)" 179 return 1 180 fi 181 log_info " state history: $state_count states" 182 183 # Check final state 184 local final_state 185 final_state=$(docker exec "$POSTGRES_CONTAINER" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -t -c \ 186 "SELECT type FROM flow_run_state WHERE flow_run_id='$flow_run_id' ORDER BY timestamp DESC LIMIT 1" 2>/dev/null | tr -d ' ') 187 log_info " final state: $final_state" 188 189 # Count total rows in key tables 190 local total_flows total_runs total_states 191 total_flows=$(docker exec "$POSTGRES_CONTAINER" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -t -c \ 192 "SELECT COUNT(*) FROM flow" 2>/dev/null | tr -d ' ') 193 total_runs=$(docker exec "$POSTGRES_CONTAINER" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -t -c \ 194 "SELECT COUNT(*) FROM flow_run" 2>/dev/null | tr -d ' ') 195 total_states=$(docker exec "$POSTGRES_CONTAINER" psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -t -c \ 196 "SELECT COUNT(*) FROM flow_run_state" 2>/dev/null | tr -d ' ') 197 log_info " table counts: flows=$total_flows, runs=$total_runs, states=$total_states" 198 199 return 0 200} 201 202test_sqlite() { 203 log_info "=== Testing SQLite backend ===" 204 205 export PREFECT_DATABASE_BACKEND=sqlite 206 export PREFECT_DATABASE_PATH="$TEST_DB_PATH" 207 208 # Remove any existing test database 209 rm -f "$TEST_DB_PATH" 210 211 # Build and run tests 212 cd "$PROJECT_DIR" 213 214 log_step "Building..." 215 zig build 2>&1 || { log_error "Build failed"; return 1; } 216 217 log_step "Running backend unit tests..." 218 zig build test 2>&1 || { log_error "Unit tests failed"; return 1; } 219 220 log_step "Starting server for integration tests..." 221 ./zig-out/bin/prefect-server & 222 SERVER_PID=$! 223 sleep 2 224 225 # Basic health check 226 log_step "Health check..." 227 if curl -s http://localhost:4200/api/health | grep -q "ok"; then 228 log_info "Health check passed" 229 else 230 log_error "Health check failed" 231 return 1 232 fi 233 234 # Test flow creation 235 log_step "Testing flow creation..." 236 FLOW_RESPONSE=$(curl -s -X POST http://localhost:4200/api/flows/ \ 237 -H "Content-Type: application/json" \ 238 -d '{"name": "test-flow-sqlite"}') 239 240 if echo "$FLOW_RESPONSE" | grep -q '"id"'; then 241 log_info "Flow creation passed" 242 FLOW_ID=$(echo "$FLOW_RESPONSE" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) 243 else 244 log_error "Flow creation failed: $FLOW_RESPONSE" 245 return 1 246 fi 247 248 # Test flow retrieval 249 log_step "Testing flow retrieval..." 250 FLOW_GET=$(curl -s "http://localhost:4200/api/flows/$FLOW_ID") 251 if echo "$FLOW_GET" | grep -q "test-flow-sqlite"; then 252 log_info "Flow retrieval passed" 253 else 254 log_error "Flow retrieval failed: $FLOW_GET" 255 return 1 256 fi 257 258 # Test flow run creation 259 log_step "Testing flow run creation..." 260 FLOW_RUN_RESPONSE=$(curl -s -X POST http://localhost:4200/api/flow_runs/ \ 261 -H "Content-Type: application/json" \ 262 -d "{\"flow_id\": \"$FLOW_ID\", \"name\": \"test-run-1\"}") 263 264 if echo "$FLOW_RUN_RESPONSE" | grep -q '"id"'; then 265 log_info "Flow run creation passed" 266 FLOW_RUN_ID=$(echo "$FLOW_RUN_RESPONSE" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) 267 else 268 log_error "Flow run creation failed: $FLOW_RUN_RESPONSE" 269 return 1 270 fi 271 272 # Test state transition: PENDING -> RUNNING 273 log_step "Testing state transition (PENDING -> RUNNING)..." 274 STATE_RESPONSE=$(curl -s -X POST "http://localhost:4200/api/flow_runs/$FLOW_RUN_ID/set_state" \ 275 -H "Content-Type: application/json" \ 276 -d '{"state": {"type": "RUNNING", "name": "Running"}}') 277 278 if echo "$STATE_RESPONSE" | grep -q "RUNNING"; then 279 log_info "State transition to RUNNING passed" 280 else 281 log_error "State transition failed: $STATE_RESPONSE" 282 return 1 283 fi 284 285 # Test state transition: RUNNING -> COMPLETED 286 log_step "Testing state transition (RUNNING -> COMPLETED)..." 287 STATE_RESPONSE=$(curl -s -X POST "http://localhost:4200/api/flow_runs/$FLOW_RUN_ID/set_state" \ 288 -H "Content-Type: application/json" \ 289 -d '{"state": {"type": "COMPLETED", "name": "Completed"}}') 290 291 if echo "$STATE_RESPONSE" | grep -q "COMPLETED"; then 292 log_info "State transition to COMPLETED passed" 293 else 294 log_error "State transition failed: $STATE_RESPONSE" 295 return 1 296 fi 297 298 # Test block type creation 299 log_step "Testing block type creation..." 300 BLOCK_TYPE_RESPONSE=$(curl -s -X POST http://localhost:4200/api/block_types/ \ 301 -H "Content-Type: application/json" \ 302 -d '{"name": "TestBlock", "slug": "test-block"}') 303 304 if echo "$BLOCK_TYPE_RESPONSE" | grep -q '"id"'; then 305 log_info "Block type creation passed" 306 else 307 log_error "Block type creation failed: $BLOCK_TYPE_RESPONSE" 308 return 1 309 fi 310 311 # Stop server before sanity checks 312 kill "$SERVER_PID" 2>/dev/null || true 313 wait "$SERVER_PID" 2>/dev/null || true 314 SERVER_PID="" 315 316 # Run sanity checks 317 sanity_check_sqlite "$TEST_DB_PATH" "$FLOW_ID" "$FLOW_RUN_ID" || return 1 318 319 log_info "=== SQLite backend tests PASSED ===" 320 return 0 321} 322 323test_postgres() { 324 log_info "=== Testing PostgreSQL backend ===" 325 326 # Start PostgreSQL via Docker 327 start_postgres_docker || return 1 328 329 export PREFECT_DATABASE_BACKEND=postgres 330 export PREFECT_DATABASE_URL="$POSTGRES_URL" 331 332 cd "$PROJECT_DIR" 333 334 log_step "Building..." 335 zig build 2>&1 || { log_error "Build failed"; stop_postgres_docker; return 1; } 336 337 log_step "Starting server..." 338 ./zig-out/bin/prefect-server & 339 SERVER_PID=$! 340 sleep 3 341 342 # Check if server started 343 if ! kill -0 $SERVER_PID 2>/dev/null; then 344 log_error "Server failed to start - check backend implementation" 345 stop_postgres_docker 346 return 1 347 fi 348 349 # Health check 350 log_step "Health check..." 351 if curl -s http://localhost:4200/api/health | grep -q "ok"; then 352 log_info "Health check passed" 353 else 354 log_error "Health check failed" 355 stop_postgres_docker 356 return 1 357 fi 358 359 # Test flow creation 360 log_step "Testing flow creation..." 361 FLOW_RESPONSE=$(curl -s -X POST http://localhost:4200/api/flows/ \ 362 -H "Content-Type: application/json" \ 363 -d '{"name": "test-flow-postgres"}') 364 365 if echo "$FLOW_RESPONSE" | grep -q '"id"'; then 366 log_info "Flow creation passed" 367 FLOW_ID=$(echo "$FLOW_RESPONSE" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) 368 else 369 log_error "Flow creation failed: $FLOW_RESPONSE" 370 stop_postgres_docker 371 return 1 372 fi 373 374 # Test flow retrieval 375 log_step "Testing flow retrieval..." 376 FLOW_GET=$(curl -s "http://localhost:4200/api/flows/$FLOW_ID") 377 if echo "$FLOW_GET" | grep -q "test-flow-postgres"; then 378 log_info "Flow retrieval passed" 379 else 380 log_error "Flow retrieval failed: $FLOW_GET" 381 stop_postgres_docker 382 return 1 383 fi 384 385 # Test flow run creation 386 log_step "Testing flow run creation..." 387 FLOW_RUN_RESPONSE=$(curl -s -X POST http://localhost:4200/api/flow_runs/ \ 388 -H "Content-Type: application/json" \ 389 -d "{\"flow_id\": \"$FLOW_ID\", \"name\": \"test-run-pg-1\"}") 390 391 if echo "$FLOW_RUN_RESPONSE" | grep -q '"id"'; then 392 log_info "Flow run creation passed" 393 FLOW_RUN_ID=$(echo "$FLOW_RUN_RESPONSE" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4) 394 else 395 log_error "Flow run creation failed: $FLOW_RUN_RESPONSE" 396 stop_postgres_docker 397 return 1 398 fi 399 400 # Test state transition: PENDING -> RUNNING 401 log_step "Testing state transition (PENDING -> RUNNING)..." 402 STATE_RESPONSE=$(curl -s -X POST "http://localhost:4200/api/flow_runs/$FLOW_RUN_ID/set_state" \ 403 -H "Content-Type: application/json" \ 404 -d '{"state": {"type": "RUNNING", "name": "Running"}}') 405 406 if echo "$STATE_RESPONSE" | grep -q "RUNNING"; then 407 log_info "State transition to RUNNING passed" 408 else 409 log_error "State transition failed: $STATE_RESPONSE" 410 stop_postgres_docker 411 return 1 412 fi 413 414 # Test state transition: RUNNING -> COMPLETED 415 log_step "Testing state transition (RUNNING -> COMPLETED)..." 416 STATE_RESPONSE=$(curl -s -X POST "http://localhost:4200/api/flow_runs/$FLOW_RUN_ID/set_state" \ 417 -H "Content-Type: application/json" \ 418 -d '{"state": {"type": "COMPLETED", "name": "Completed"}}') 419 420 if echo "$STATE_RESPONSE" | grep -q "COMPLETED"; then 421 log_info "State transition to COMPLETED passed" 422 else 423 log_error "State transition failed: $STATE_RESPONSE" 424 stop_postgres_docker 425 return 1 426 fi 427 428 # Test block type creation 429 log_step "Testing block type creation..." 430 BLOCK_TYPE_RESPONSE=$(curl -s -X POST http://localhost:4200/api/block_types/ \ 431 -H "Content-Type: application/json" \ 432 -d '{"name": "TestBlockPG", "slug": "test-block-pg"}') 433 434 if echo "$BLOCK_TYPE_RESPONSE" | grep -q '"id"'; then 435 log_info "Block type creation passed" 436 else 437 log_error "Block type creation failed: $BLOCK_TYPE_RESPONSE" 438 stop_postgres_docker 439 return 1 440 fi 441 442 # Stop server before sanity checks 443 kill "$SERVER_PID" 2>/dev/null || true 444 wait "$SERVER_PID" 2>/dev/null || true 445 SERVER_PID="" 446 447 # Run sanity checks 448 sanity_check_postgres "$FLOW_ID" "$FLOW_RUN_ID" || { 449 stop_postgres_docker 450 return 1 451 } 452 453 # Clean up Docker 454 stop_postgres_docker 455 456 log_info "=== PostgreSQL backend tests PASSED ===" 457 return 0 458} 459 460main() { 461 case "$BACKEND" in 462 sqlite) 463 test_sqlite 464 ;; 465 postgres|postgresql) 466 test_postgres 467 ;; 468 all) 469 log_info "Running all backend tests..." 470 echo "" 471 test_sqlite || exit 1 472 echo "" 473 test_postgres || exit 1 474 echo "" 475 log_info "=== ALL BACKEND TESTS PASSED ===" 476 ;; 477 *) 478 log_error "Unknown backend: $BACKEND" 479 echo "Usage: $0 [sqlite|postgres|all]" 480 exit 1 481 ;; 482 esac 483} 484 485main