]> begriffs open source - pg_scribe/blob - tests/test_start.sh
More little refactors
[pg_scribe] / tests / test_start.sh
1 #!/usr/bin/env bash
2 #
3 # Test suite for pg_scribe --start command
4 #
5 # This test suite:
6 # - Creates temporary test databases
7 # - Tests various --start scenarios
8 # - Verifies SQL capture (DML + DDL)
9 # - Tests signal handling
10 # - Cleans up all resources
11 #
12
13 set -euo pipefail
14
15 # Colors for test output
16 RED='\033[0;31m'
17 GREEN='\033[0;32m'
18 YELLOW='\033[0;33m'
19 BLUE='\033[0;34m'
20 NC='\033[0m' # No Color
21
22 # Test configuration
23 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
24 PG_SCRIBE="$SCRIPT_DIR/scripts/pg_scribe"
25 TEST_DIR="/tmp/pg_scribe_test_start_$$"
26 TEST_DB_PREFIX="pg_scribe_test_start_$$"
27 PGUSER="${PGUSER:-postgres}"
28
29 # Test counters
30 TESTS_RUN=0
31 TESTS_PASSED=0
32 TESTS_FAILED=0
33
34 # Cleanup tracking
35 DATABASES_TO_CLEANUP=()
36 PIDS_TO_CLEANUP=()
37
38 #
39 # Logging functions
40 #
41
42 log_test() {
43     echo -e "${BLUE}TEST:${NC} $*"
44 }
45
46 log_pass() {
47     echo -e "${GREEN}PASS:${NC} $*"
48     ((TESTS_PASSED++))
49 }
50
51 log_fail() {
52     echo -e "${RED}FAIL:${NC} $*"
53     ((TESTS_FAILED++))
54 }
55
56 log_info() {
57     echo -e "${YELLOW}INFO:${NC} $*"
58 }
59
60 #
61 # Helper functions
62 #
63
64 run_psql() {
65     local dbname="$1"
66     shift
67     psql -U "$PGUSER" -d "$dbname" -tAq "$@"
68 }
69
70 query_db() {
71     local dbname="$1"
72     local query="$2"
73     run_psql "$dbname" -c "$query" 2>/dev/null || true
74 }
75
76 create_test_db() {
77     local dbname="$1"
78     log_info "Creating test database: $dbname"
79
80     # Drop if exists
81     psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $dbname;" &>/dev/null || true
82
83     # Create database
84     psql -U "$PGUSER" -d postgres -c "CREATE DATABASE $dbname;" &>/dev/null
85
86     DATABASES_TO_CLEANUP+=("$dbname")
87 }
88
89 # shellcheck disable=SC2317  # Function called from cleanup trap handler
90 drop_test_db() {
91     local dbname="$1"
92     log_info "Dropping test database: $dbname"
93
94     # Terminate connections
95     psql -U "$PGUSER" -d postgres -c "
96         SELECT pg_terminate_backend(pid)
97         FROM pg_stat_activity
98         WHERE datname = '$dbname' AND pid <> pg_backend_pid();
99     " &>/dev/null || true
100
101     # Drop database
102     psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $dbname;" &>/dev/null || true
103 }
104
105 # shellcheck disable=SC2317  # Function called from cleanup trap handler
106 drop_replication_slot() {
107     local dbname="$1"
108     local slot="$2"
109     log_info "Dropping replication slot: $slot"
110
111     # Check if slot exists
112     local exists
113     exists=$(query_db "$dbname" "
114         SELECT 1 FROM pg_replication_slots WHERE slot_name = '$slot';
115     ")
116
117     if [[ -n "$exists" ]]; then
118         # Drop slot
119         query_db "$dbname" "SELECT pg_drop_replication_slot('$slot');" || true
120     fi
121 }
122
123 check_slot_exists() {
124     local dbname="$1"
125     local slot="$2"
126     local exists
127     exists=$(query_db "$dbname" "
128         SELECT 1 FROM pg_replication_slots WHERE slot_name = '$slot';
129     ")
130     [[ -n "$exists" ]]
131 }
132
133 create_table_with_pk() {
134     local dbname="$1"
135     local table="$2"
136     query_db "$dbname" "
137         CREATE TABLE $table (
138             id SERIAL PRIMARY KEY,
139             name TEXT,
140             created_at TIMESTAMP DEFAULT now()
141         );
142     "
143 }
144
145 initialize_backup_system() {
146     local dbname="$1"
147     local slot="$2"
148     local backup_dir="$3"
149
150     # Create backup directory
151     mkdir -p "$backup_dir"
152
153     # Initialize
154     "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null
155 }
156
157 #
158 # Test cases
159 #
160
161 test_start_without_init() {
162     ((TESTS_RUN++))
163     log_test "Start without initialization (should fail)"
164
165     local dbname="${TEST_DB_PREFIX}_noinit"
166     local slot="test_slot_noinit"
167     local output_file="$TEST_DIR/noinit.sql"
168
169     # Setup - create db but DON'T initialize
170     create_test_db "$dbname"
171     create_table_with_pk "$dbname" "users"
172
173     # Try to start - should fail with exit code 3 (slot error)
174     local exit_code=0
175     "$PG_SCRIBE" --start -d "$dbname" -f "$output_file" -S "$slot" -U "$PGUSER" &>/dev/null || exit_code=$?
176
177     if [[ $exit_code -eq 3 ]]; then
178         log_pass "Correctly failed with slot error"
179         return 0
180     else
181         log_fail "Expected exit code 3, got $exit_code"
182         return 1
183     fi
184 }
185
186 test_start_basic_streaming() {
187     ((TESTS_RUN++))
188     log_test "Basic streaming with DML capture"
189
190     local dbname="${TEST_DB_PREFIX}_basic"
191     local slot="test_slot_basic"
192     local backup_dir="$TEST_DIR/basic"
193     local output_file="$TEST_DIR/basic_stream.sql"
194
195     # Setup
196     create_test_db "$dbname"
197     create_table_with_pk "$dbname" "users"
198     initialize_backup_system "$dbname" "$slot" "$backup_dir"
199
200     # Start streaming in background
201     "$PG_SCRIBE" --start -d "$dbname" -f "$output_file" -S "$slot" -U "$PGUSER" -s 1 -F 1 &>/dev/null &
202     local pg_scribe_pid=$!
203     PIDS_TO_CLEANUP+=("$pg_scribe_pid")
204
205     # Give it a moment to start
206     sleep 1
207
208     # Make some changes
209     query_db "$dbname" "INSERT INTO users (name) VALUES ('Alice');"
210     query_db "$dbname" "INSERT INTO users (name) VALUES ('Bob');"
211     query_db "$dbname" "UPDATE users SET name = 'Alice Smith' WHERE name = 'Alice';"
212     query_db "$dbname" "DELETE FROM users WHERE name = 'Bob';"
213
214     # Give it time to flush (status-interval=1, fsync-interval=1)
215     sleep 2
216
217     # Stop streaming
218     kill -INT "$pg_scribe_pid" 2>/dev/null || true
219     sleep 1
220
221     # Force kill if still running
222     if kill -0 "$pg_scribe_pid" 2>/dev/null; then
223         kill -9 "$pg_scribe_pid" 2>/dev/null || true
224     fi
225     wait "$pg_scribe_pid" 2>/dev/null || true
226
227     # Verify output file exists
228     if [[ ! -f "$output_file" ]]; then
229         log_fail "Output file not created"
230         return 1
231     fi
232
233     # Verify SQL content
234     if ! grep -q "INSERT INTO public.users" "$output_file"; then
235         log_fail "INSERT not captured"
236         return 1
237     fi
238
239     if ! grep -q "UPDATE public.users" "$output_file"; then
240         log_fail "UPDATE not captured"
241         return 1
242     fi
243
244     if ! grep -q "DELETE FROM public.users" "$output_file"; then
245         log_fail "DELETE not captured"
246         return 1
247     fi
248
249     # Verify transaction boundaries
250     if ! grep -q "BEGIN" "$output_file"; then
251         log_fail "BEGIN not captured"
252         return 1
253     fi
254
255     if ! grep -q "COMMIT" "$output_file"; then
256         log_fail "COMMIT not captured"
257         return 1
258     fi
259
260     log_pass "DML captured successfully"
261     return 0
262 }
263
264 test_start_ddl_capture() {
265     ((TESTS_RUN++))
266     log_test "DDL capture via event triggers"
267
268     local dbname="${TEST_DB_PREFIX}_ddl"
269     local slot="test_slot_ddl"
270     local backup_dir="$TEST_DIR/ddl"
271     local output_file="$TEST_DIR/ddl_stream.sql"
272
273     # Setup
274     create_test_db "$dbname"
275     create_table_with_pk "$dbname" "users"
276     initialize_backup_system "$dbname" "$slot" "$backup_dir"
277
278     # Start streaming in background
279     "$PG_SCRIBE" --start -d "$dbname" -f "$output_file" -S "$slot" -U "$PGUSER" -s 1 -F 1 &>/dev/null &
280     local pg_scribe_pid=$!
281     PIDS_TO_CLEANUP+=("$pg_scribe_pid")
282
283     # Give it a moment to start
284     sleep 1
285
286     # Make DDL changes
287     query_db "$dbname" "CREATE TABLE products (id SERIAL PRIMARY KEY, name TEXT);"
288     query_db "$dbname" "ALTER TABLE products ADD COLUMN price NUMERIC(10,2);"
289     query_db "$dbname" "DROP TABLE products;"
290
291     # Give it time to flush
292     sleep 2
293
294     # Stop streaming
295     kill -INT "$pg_scribe_pid" 2>/dev/null || true
296     sleep 1
297
298     # Force kill if still running
299     if kill -0 "$pg_scribe_pid" 2>/dev/null; then
300         kill -9 "$pg_scribe_pid" 2>/dev/null || true
301     fi
302     wait "$pg_scribe_pid" 2>/dev/null || true
303
304     # Verify DDL captured
305     if ! grep -qi "CREATE TABLE products" "$output_file"; then
306         log_fail "CREATE TABLE not captured"
307         return 1
308     fi
309
310     if ! grep -qi "ALTER TABLE products" "$output_file"; then
311         log_fail "ALTER TABLE not captured"
312         return 1
313     fi
314
315     if ! grep -qi "DROP TABLE products" "$output_file"; then
316         log_fail "DROP TABLE not captured"
317         return 1
318     fi
319
320     log_pass "DDL captured successfully"
321     return 0
322 }
323
324 test_start_truncate_capture() {
325     ((TESTS_RUN++))
326     log_test "TRUNCATE capture"
327
328     local dbname="${TEST_DB_PREFIX}_truncate"
329     local slot="test_slot_truncate"
330     local backup_dir="$TEST_DIR/truncate"
331     local output_file="$TEST_DIR/truncate_stream.sql"
332
333     # Setup
334     create_test_db "$dbname"
335     create_table_with_pk "$dbname" "users"
336     initialize_backup_system "$dbname" "$slot" "$backup_dir"
337
338     # Start streaming in background
339     "$PG_SCRIBE" --start -d "$dbname" -f "$output_file" -S "$slot" -U "$PGUSER" -s 1 -F 1 &>/dev/null &
340     local pg_scribe_pid=$!
341     PIDS_TO_CLEANUP+=("$pg_scribe_pid")
342
343     # Give it a moment to start
344     sleep 1
345
346     # Insert data and truncate
347     query_db "$dbname" "INSERT INTO users (name) VALUES ('Alice'), ('Bob'), ('Charlie');"
348     query_db "$dbname" "TRUNCATE users;"
349
350     # Give it time to flush
351     sleep 2
352
353     # Stop streaming
354     kill -INT "$pg_scribe_pid" 2>/dev/null || true
355     sleep 1
356
357     # Force kill if still running
358     if kill -0 "$pg_scribe_pid" 2>/dev/null; then
359         kill -9 "$pg_scribe_pid" 2>/dev/null || true
360     fi
361     wait "$pg_scribe_pid" 2>/dev/null || true
362
363     # Verify TRUNCATE captured
364     if ! grep -qi "TRUNCATE.*users" "$output_file"; then
365         log_fail "TRUNCATE not captured"
366         return 1
367     fi
368
369     log_pass "TRUNCATE captured successfully"
370     return 0
371 }
372
373 test_start_signal_handling() {
374     ((TESTS_RUN++))
375     log_test "Signal handling (SIGTERM)"
376
377     local dbname="${TEST_DB_PREFIX}_signal"
378     local slot="test_slot_signal"
379     local backup_dir="$TEST_DIR/signal"
380     local output_file="$TEST_DIR/signal_stream.sql"
381
382     # Setup
383     create_test_db "$dbname"
384     create_table_with_pk "$dbname" "users"
385     initialize_backup_system "$dbname" "$slot" "$backup_dir"
386
387     # Start streaming in background
388     "$PG_SCRIBE" --start -d "$dbname" -f "$output_file" -S "$slot" -U "$PGUSER" -s 1 -F 1 &>/dev/null &
389     local pg_scribe_pid=$!
390     PIDS_TO_CLEANUP+=("$pg_scribe_pid")
391
392     # Give it a moment to start
393     sleep 1
394
395     # Make a change
396     query_db "$dbname" "INSERT INTO users (name) VALUES ('Test');"
397
398     # Give it time to flush
399     sleep 1
400
401     # Send SIGTERM
402     kill -TERM "$pg_scribe_pid" 2>/dev/null || true
403
404     # Wait for graceful shutdown (with timeout)
405     local timeout=3
406     local count=0
407     while kill -0 "$pg_scribe_pid" 2>/dev/null && [[ $count -lt $timeout ]]; do
408         sleep 1
409         ((count++))
410     done
411
412     # Check if process stopped
413     if kill -0 "$pg_scribe_pid" 2>/dev/null; then
414         log_fail "Process did not stop after SIGTERM"
415         kill -9 "$pg_scribe_pid" 2>/dev/null || true
416         return 1
417     fi
418
419     # Verify output file was created and flushed
420     if [[ ! -f "$output_file" ]]; then
421         log_fail "Output file not created"
422         return 1
423     fi
424
425     if ! grep -q "INSERT INTO public.users" "$output_file"; then
426         log_fail "Data not flushed before shutdown"
427         return 1
428     fi
429
430     log_pass "SIGTERM handled gracefully"
431     return 0
432 }
433
434 test_start_interleaved_ddl_dml() {
435     ((TESTS_RUN++))
436     log_test "DDL and DML interleaving (chronological order)"
437
438     local dbname="${TEST_DB_PREFIX}_interleaved"
439     local slot="test_slot_interleaved"
440     local backup_dir="$TEST_DIR/interleaved"
441     local output_file="$TEST_DIR/interleaved_stream.sql"
442
443     # Setup
444     create_test_db "$dbname"
445     create_table_with_pk "$dbname" "users"
446     initialize_backup_system "$dbname" "$slot" "$backup_dir"
447
448     # Start streaming in background
449     "$PG_SCRIBE" --start -d "$dbname" -f "$output_file" -S "$slot" -U "$PGUSER" -s 1 -F 1 &>/dev/null &
450     local pg_scribe_pid=$!
451     PIDS_TO_CLEANUP+=("$pg_scribe_pid")
452
453     # Give it a moment to start
454     sleep 1
455
456     # Make interleaved changes
457     query_db "$dbname" "INSERT INTO users (name) VALUES ('Alice');"
458     query_db "$dbname" "ALTER TABLE users ADD COLUMN email TEXT;"
459     query_db "$dbname" "UPDATE users SET email = 'alice@example.com' WHERE name = 'Alice';"
460     query_db "$dbname" "ALTER TABLE users DROP COLUMN created_at;"
461     query_db "$dbname" "INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');"
462
463     # Give it time to flush
464     sleep 2
465
466     # Stop streaming
467     kill -INT "$pg_scribe_pid" 2>/dev/null || true
468     sleep 1
469
470     # Force kill if still running
471     if kill -0 "$pg_scribe_pid" 2>/dev/null; then
472         kill -9 "$pg_scribe_pid" 2>/dev/null || true
473     fi
474     wait "$pg_scribe_pid" 2>/dev/null || true
475
476     # Verify all operations captured
477     if ! grep -q "INSERT INTO public.users (id, name" "$output_file"; then
478         log_fail "First INSERT not captured"
479         return 1
480     fi
481
482     if ! grep -qi "ALTER TABLE.*ADD COLUMN email" "$output_file"; then
483         log_fail "ALTER TABLE ADD COLUMN not captured"
484         return 1
485     fi
486
487     if ! grep -q "UPDATE public.users.*email" "$output_file"; then
488         log_fail "UPDATE with new column not captured"
489         return 1
490     fi
491
492     if ! grep -qi "ALTER TABLE.*DROP COLUMN" "$output_file"; then
493         log_fail "ALTER TABLE DROP COLUMN not captured"
494         return 1
495     fi
496
497     # Verify chronological order by checking line numbers
498     local insert1_line
499     insert1_line=$(grep -n "INSERT INTO public.users (id, name" "$output_file" | head -1 | cut -d: -f1)
500     local alter_add_line
501     alter_add_line=$(grep -ni "ALTER TABLE.*ADD COLUMN email" "$output_file" | cut -d: -f1)
502     local update_line
503     update_line=$(grep -n "UPDATE public.users.*email" "$output_file" | cut -d: -f1)
504
505     if [[ "$insert1_line" -gt "$alter_add_line" ]]; then
506         log_fail "DDL/DML not in chronological order (INSERT after ALTER)"
507         return 1
508     fi
509
510     if [[ "$alter_add_line" -gt "$update_line" ]]; then
511         log_fail "DDL/DML not in chronological order (ALTER after UPDATE)"
512         return 1
513     fi
514
515     log_pass "DDL/DML interleaved in correct chronological order"
516     return 0
517 }
518
519 test_start_stdout_output() {
520     ((TESTS_RUN++))
521     log_test "Output to stdout with -f -"
522
523     local dbname="${TEST_DB_PREFIX}_stdout"
524     local slot="test_slot_stdout"
525     local backup_dir="$TEST_DIR/stdout"
526     local output_file="$TEST_DIR/stdout_capture.sql"
527
528     # Setup
529     create_test_db "$dbname"
530     create_table_with_pk "$dbname" "users"
531     initialize_backup_system "$dbname" "$slot" "$backup_dir"
532
533     # Start streaming to stdout (capture to file)
534     "$PG_SCRIBE" --start -d "$dbname" -f - -S "$slot" -U "$PGUSER" -s 1 -F 1 2>/dev/null > "$output_file" &
535     local pg_scribe_pid=$!
536     PIDS_TO_CLEANUP+=("$pg_scribe_pid")
537
538     # Give it a moment to start
539     sleep 1
540
541     # Make a change
542     query_db "$dbname" "INSERT INTO users (name) VALUES ('Stdout Test');"
543
544     # Give it time to flush
545     sleep 2
546
547     # Stop streaming
548     kill -INT "$pg_scribe_pid" 2>/dev/null || true
549     sleep 1
550
551     # Force kill if still running
552     if kill -0 "$pg_scribe_pid" 2>/dev/null; then
553         kill -9 "$pg_scribe_pid" 2>/dev/null || true
554     fi
555     wait "$pg_scribe_pid" 2>/dev/null || true
556
557     # Verify output
558     if ! grep -q "INSERT INTO public.users" "$output_file"; then
559         log_fail "Stdout output not captured"
560         return 1
561     fi
562
563     log_pass "Stdout output works correctly"
564     return 0
565 }
566
567 test_start_log_rotation() {
568     ((TESTS_RUN++))
569     log_test "Log rotation with SIGHUP"
570
571     local dbname="${TEST_DB_PREFIX}_rotation"
572     local slot="test_slot_rotation"
573     local backup_dir="$TEST_DIR/rotation"
574     local output_file="$TEST_DIR/rotation.sql"
575     local rotated_file="$TEST_DIR/rotation.sql.old"
576
577     # Setup
578     create_test_db "$dbname"
579     create_table_with_pk "$dbname" "users"
580     initialize_backup_system "$dbname" "$slot" "$backup_dir"
581
582     # Start streaming in background
583     "$PG_SCRIBE" --start -d "$dbname" -f "$output_file" -S "$slot" -U "$PGUSER" -s 1 -F 1 &>/dev/null &
584     local pg_scribe_pid=$!
585     PIDS_TO_CLEANUP+=("$pg_scribe_pid")
586
587     # Give it a moment to start
588     sleep 1
589
590     # Make changes before rotation
591     query_db "$dbname" "INSERT INTO users (name) VALUES ('Before Rotation');"
592
593     # Give it time to flush
594     sleep 2
595
596     # Rotate the log file
597     mv "$output_file" "$rotated_file"
598
599     # Send SIGHUP to trigger file reopening
600     kill -HUP "$pg_scribe_pid" 2>/dev/null || true
601
602     # Give it time to reopen the file
603     sleep 1
604
605     # Make changes after rotation
606     query_db "$dbname" "INSERT INTO users (name) VALUES ('After Rotation');"
607
608     # Give it time to flush
609     sleep 2
610
611     # Stop streaming
612     kill -INT "$pg_scribe_pid" 2>/dev/null || true
613     sleep 1
614
615     # Force kill if still running
616     if kill -0 "$pg_scribe_pid" 2>/dev/null; then
617         kill -9 "$pg_scribe_pid" 2>/dev/null || true
618     fi
619     wait "$pg_scribe_pid" 2>/dev/null || true
620
621     # Verify old file has "Before Rotation" but not "After Rotation"
622     if ! grep -q "Before Rotation" "$rotated_file"; then
623         log_fail "Old file missing data before rotation"
624         return 1
625     fi
626
627     if grep -q "After Rotation" "$rotated_file"; then
628         log_fail "Old file should not contain data after rotation"
629         return 1
630     fi
631
632     # Verify new file has "After Rotation" but not "Before Rotation"
633     if [[ ! -f "$output_file" ]]; then
634         log_fail "New file not created after rotation"
635         return 1
636     fi
637
638     if ! grep -q "After Rotation" "$output_file"; then
639         log_fail "New file missing data after rotation"
640         return 1
641     fi
642
643     if grep -q "Before Rotation" "$output_file"; then
644         log_fail "New file should not contain data before rotation"
645         return 1
646     fi
647
648     log_pass "Log rotation with SIGHUP works correctly"
649     return 0
650 }
651
652 #
653 # Cleanup
654 #
655
656 # shellcheck disable=SC2317  # Function called via trap handler
657 cleanup() {
658     log_info "Cleaning up test resources..."
659
660     # Kill any running pg_scribe processes
661     for pid in "${PIDS_TO_CLEANUP[@]}"; do
662         if kill -0 "$pid" 2>/dev/null; then
663             log_info "Stopping pg_scribe process $pid"
664             # Try graceful shutdown first (allows signal forwarding to child processes)
665             kill -TERM "$pid" 2>/dev/null || true
666
667             # Wait briefly for graceful shutdown
668             local timeout=2
669             local count=0
670             while kill -0 "$pid" 2>/dev/null && [[ $count -lt $timeout ]]; do
671                 sleep 0.5
672                 ((count++))
673             done
674
675             # Force kill if still running
676             if kill -0 "$pid" 2>/dev/null; then
677                 log_info "Force killing pg_scribe process $pid"
678                 kill -9 "$pid" 2>/dev/null || true
679             fi
680         fi
681     done
682
683     # Wait for child pg_recvlogical processes to fully terminate
684     # (They may take a moment to shut down after parent terminates)
685     sleep 1
686
687     # Drop replication slots
688     for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
689         for slot in test_slot_noinit test_slot_basic test_slot_ddl test_slot_truncate test_slot_signal test_slot_interleaved test_slot_stdout test_slot_rotation; do
690             drop_replication_slot "$dbname" "$slot" 2>/dev/null || true
691         done
692     done
693
694     # Drop databases
695     for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
696         drop_test_db "$dbname"
697     done
698
699     # Remove test directory
700     if [[ -d "$TEST_DIR" ]]; then
701         rm -rf "$TEST_DIR"
702     fi
703
704     log_info "Cleanup complete"
705 }
706
707 #
708 # Main test runner
709 #
710
711 main() {
712     echo "========================================"
713     echo "pg_scribe --start Test Suite"
714     echo "========================================"
715     echo ""
716
717     # Verify pg_scribe exists
718     if [[ ! -x "$PG_SCRIBE" ]]; then
719         echo "ERROR: pg_scribe not found or not executable: $PG_SCRIBE"
720         exit 1
721     fi
722
723     # Verify PostgreSQL is running
724     if ! psql -U "$PGUSER" -d postgres -c "SELECT 1;" &>/dev/null; then
725         echo "ERROR: Cannot connect to PostgreSQL"
726         exit 1
727     fi
728
729     # Verify wal_level is logical
730     local wal_level
731     wal_level=$(psql -U "$PGUSER" -d postgres -tAq -c "SHOW wal_level;")
732     if [[ "$wal_level" != "logical" ]]; then
733         echo "ERROR: wal_level must be 'logical', currently: $wal_level"
734         echo "Update ~/.pgenv/pgsql/data/postgresql.conf and restart PostgreSQL"
735         exit 1
736     fi
737
738     # Verify wal2sql extension is available
739     if ! pg_config --pkglibdir &>/dev/null; then
740         echo "ERROR: pg_config not found"
741         exit 1
742     fi
743
744     local wal2sql_path
745     wal2sql_path="$(pg_config --pkglibdir)/wal2sql.so"
746     if [[ ! -f "$wal2sql_path" ]]; then
747         echo "ERROR: wal2sql.so not found at $wal2sql_path"
748         echo "Build and install wal2sql: cd wal2sql && make && make install"
749         exit 1
750     fi
751
752     # Create test directory
753     mkdir -p "$TEST_DIR"
754
755     # Set up cleanup trap
756     trap cleanup EXIT INT TERM
757
758     echo "Running tests..."
759     echo ""
760
761     # Run all tests (use || true to prevent set -e from exiting)
762     test_start_without_init || true
763     test_start_basic_streaming || true
764     test_start_ddl_capture || true
765     test_start_truncate_capture || true
766     test_start_signal_handling || true
767     test_start_interleaved_ddl_dml || true
768     test_start_stdout_output || true
769     test_start_log_rotation || true
770
771     # Summary
772     echo ""
773     echo "========================================"
774     echo "Test Results"
775     echo "========================================"
776     echo "Tests run:    $TESTS_RUN"
777     echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}"
778     echo -e "Tests failed: ${RED}$TESTS_FAILED${NC}"
779     echo ""
780
781     if [[ $TESTS_FAILED -eq 0 ]]; then
782         echo -e "${GREEN}All tests passed!${NC}"
783         exit 0
784     else
785         echo -e "${RED}Some tests failed!${NC}"
786         exit 1
787     fi
788 }
789
790 main "$@"