prefect server in zig
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