3 # Test suite for pg_scribe --start command
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
15 # Colors for test output
20 NC='\033[0m' # No Color
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}"
35 DATABASES_TO_CLEANUP=()
43 echo -e "${BLUE}TEST:${NC} $*"
47 echo -e "${GREEN}PASS:${NC} $*"
52 echo -e "${RED}FAIL:${NC} $*"
57 echo -e "${YELLOW}INFO:${NC} $*"
67 psql -U "$PGUSER" -d "$dbname" -tAq "$@"
73 run_psql "$dbname" -c "$query" 2>/dev/null || true
78 log_info "Creating test database: $dbname"
81 psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $dbname;" &>/dev/null || true
84 psql -U "$PGUSER" -d postgres -c "CREATE DATABASE $dbname;" &>/dev/null
86 DATABASES_TO_CLEANUP+=("$dbname")
89 # shellcheck disable=SC2317 # Function called from cleanup trap handler
92 log_info "Dropping test database: $dbname"
94 # Terminate connections
95 psql -U "$PGUSER" -d postgres -c "
96 SELECT pg_terminate_backend(pid)
98 WHERE datname = '$dbname' AND pid <> pg_backend_pid();
102 psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $dbname;" &>/dev/null || true
105 # shellcheck disable=SC2317 # Function called from cleanup trap handler
106 drop_replication_slot() {
109 log_info "Dropping replication slot: $slot"
111 # Check if slot exists
113 exists=$(query_db "$dbname" "
114 SELECT 1 FROM pg_replication_slots WHERE slot_name = '$slot';
117 if [[ -n "$exists" ]]; then
119 query_db "$dbname" "SELECT pg_drop_replication_slot('$slot');" || true
123 check_slot_exists() {
127 exists=$(query_db "$dbname" "
128 SELECT 1 FROM pg_replication_slots WHERE slot_name = '$slot';
133 create_table_with_pk() {
137 CREATE TABLE $table (
138 id SERIAL PRIMARY KEY,
140 created_at TIMESTAMP DEFAULT now()
145 initialize_backup_system() {
148 local backup_dir="$3"
150 # Create backup directory
151 mkdir -p "$backup_dir"
154 "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null
161 test_start_without_init() {
163 log_test "Start without initialization (should fail)"
165 local dbname="${TEST_DB_PREFIX}_noinit"
166 local slot="test_slot_noinit"
167 local output_file="$TEST_DIR/noinit.sql"
169 # Setup - create db but DON'T initialize
170 create_test_db "$dbname"
171 create_table_with_pk "$dbname" "users"
173 # Try to start - should fail with exit code 3 (slot error)
175 "$PG_SCRIBE" --start -d "$dbname" -f "$output_file" -S "$slot" -U "$PGUSER" &>/dev/null || exit_code=$?
177 if [[ $exit_code -eq 3 ]]; then
178 log_pass "Correctly failed with slot error"
181 log_fail "Expected exit code 3, got $exit_code"
186 test_start_basic_streaming() {
188 log_test "Basic streaming with DML capture"
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"
196 create_test_db "$dbname"
197 create_table_with_pk "$dbname" "users"
198 initialize_backup_system "$dbname" "$slot" "$backup_dir"
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")
205 # Give it a moment to start
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';"
214 # Give it time to flush (status-interval=1, fsync-interval=1)
218 kill -INT "$pg_scribe_pid" 2>/dev/null || true
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
225 wait "$pg_scribe_pid" 2>/dev/null || true
227 # Verify output file exists
228 if [[ ! -f "$output_file" ]]; then
229 log_fail "Output file not created"
234 if ! grep -q "INSERT INTO public.users" "$output_file"; then
235 log_fail "INSERT not captured"
239 if ! grep -q "UPDATE public.users" "$output_file"; then
240 log_fail "UPDATE not captured"
244 if ! grep -q "DELETE FROM public.users" "$output_file"; then
245 log_fail "DELETE not captured"
249 # Verify transaction boundaries
250 if ! grep -q "BEGIN" "$output_file"; then
251 log_fail "BEGIN not captured"
255 if ! grep -q "COMMIT" "$output_file"; then
256 log_fail "COMMIT not captured"
260 log_pass "DML captured successfully"
264 test_start_ddl_capture() {
266 log_test "DDL capture via event triggers"
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"
274 create_test_db "$dbname"
275 create_table_with_pk "$dbname" "users"
276 initialize_backup_system "$dbname" "$slot" "$backup_dir"
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")
283 # Give it a moment to start
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;"
291 # Give it time to flush
295 kill -INT "$pg_scribe_pid" 2>/dev/null || true
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
302 wait "$pg_scribe_pid" 2>/dev/null || true
304 # Verify DDL captured
305 if ! grep -qi "CREATE TABLE products" "$output_file"; then
306 log_fail "CREATE TABLE not captured"
310 if ! grep -qi "ALTER TABLE products" "$output_file"; then
311 log_fail "ALTER TABLE not captured"
315 if ! grep -qi "DROP TABLE products" "$output_file"; then
316 log_fail "DROP TABLE not captured"
320 log_pass "DDL captured successfully"
324 test_start_truncate_capture() {
326 log_test "TRUNCATE capture"
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"
334 create_test_db "$dbname"
335 create_table_with_pk "$dbname" "users"
336 initialize_backup_system "$dbname" "$slot" "$backup_dir"
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")
343 # Give it a moment to start
346 # Insert data and truncate
347 query_db "$dbname" "INSERT INTO users (name) VALUES ('Alice'), ('Bob'), ('Charlie');"
348 query_db "$dbname" "TRUNCATE users;"
350 # Give it time to flush
354 kill -INT "$pg_scribe_pid" 2>/dev/null || true
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
361 wait "$pg_scribe_pid" 2>/dev/null || true
363 # Verify TRUNCATE captured
364 if ! grep -qi "TRUNCATE.*users" "$output_file"; then
365 log_fail "TRUNCATE not captured"
369 log_pass "TRUNCATE captured successfully"
373 test_start_signal_handling() {
375 log_test "Signal handling (SIGTERM)"
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"
383 create_test_db "$dbname"
384 create_table_with_pk "$dbname" "users"
385 initialize_backup_system "$dbname" "$slot" "$backup_dir"
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")
392 # Give it a moment to start
396 query_db "$dbname" "INSERT INTO users (name) VALUES ('Test');"
398 # Give it time to flush
402 kill -TERM "$pg_scribe_pid" 2>/dev/null || true
404 # Wait for graceful shutdown (with timeout)
407 while kill -0 "$pg_scribe_pid" 2>/dev/null && [[ $count -lt $timeout ]]; do
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
419 # Verify output file was created and flushed
420 if [[ ! -f "$output_file" ]]; then
421 log_fail "Output file not created"
425 if ! grep -q "INSERT INTO public.users" "$output_file"; then
426 log_fail "Data not flushed before shutdown"
430 log_pass "SIGTERM handled gracefully"
434 test_start_interleaved_ddl_dml() {
436 log_test "DDL and DML interleaving (chronological order)"
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"
444 create_test_db "$dbname"
445 create_table_with_pk "$dbname" "users"
446 initialize_backup_system "$dbname" "$slot" "$backup_dir"
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")
453 # Give it a moment to start
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');"
463 # Give it time to flush
467 kill -INT "$pg_scribe_pid" 2>/dev/null || true
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
474 wait "$pg_scribe_pid" 2>/dev/null || true
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"
482 if ! grep -qi "ALTER TABLE.*ADD COLUMN email" "$output_file"; then
483 log_fail "ALTER TABLE ADD COLUMN not captured"
487 if ! grep -q "UPDATE public.users.*email" "$output_file"; then
488 log_fail "UPDATE with new column not captured"
492 if ! grep -qi "ALTER TABLE.*DROP COLUMN" "$output_file"; then
493 log_fail "ALTER TABLE DROP COLUMN not captured"
497 # Verify chronological order by checking line numbers
499 insert1_line=$(grep -n "INSERT INTO public.users (id, name" "$output_file" | head -1 | cut -d: -f1)
501 alter_add_line=$(grep -ni "ALTER TABLE.*ADD COLUMN email" "$output_file" | cut -d: -f1)
503 update_line=$(grep -n "UPDATE public.users.*email" "$output_file" | cut -d: -f1)
505 if [[ "$insert1_line" -gt "$alter_add_line" ]]; then
506 log_fail "DDL/DML not in chronological order (INSERT after ALTER)"
510 if [[ "$alter_add_line" -gt "$update_line" ]]; then
511 log_fail "DDL/DML not in chronological order (ALTER after UPDATE)"
515 log_pass "DDL/DML interleaved in correct chronological order"
519 test_start_stdout_output() {
521 log_test "Output to stdout with -f -"
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"
529 create_test_db "$dbname"
530 create_table_with_pk "$dbname" "users"
531 initialize_backup_system "$dbname" "$slot" "$backup_dir"
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")
538 # Give it a moment to start
542 query_db "$dbname" "INSERT INTO users (name) VALUES ('Stdout Test');"
544 # Give it time to flush
548 kill -INT "$pg_scribe_pid" 2>/dev/null || true
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
555 wait "$pg_scribe_pid" 2>/dev/null || true
558 if ! grep -q "INSERT INTO public.users" "$output_file"; then
559 log_fail "Stdout output not captured"
563 log_pass "Stdout output works correctly"
567 test_start_log_rotation() {
569 log_test "Log rotation with SIGHUP"
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"
578 create_test_db "$dbname"
579 create_table_with_pk "$dbname" "users"
580 initialize_backup_system "$dbname" "$slot" "$backup_dir"
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")
587 # Give it a moment to start
590 # Make changes before rotation
591 query_db "$dbname" "INSERT INTO users (name) VALUES ('Before Rotation');"
593 # Give it time to flush
596 # Rotate the log file
597 mv "$output_file" "$rotated_file"
599 # Send SIGHUP to trigger file reopening
600 kill -HUP "$pg_scribe_pid" 2>/dev/null || true
602 # Give it time to reopen the file
605 # Make changes after rotation
606 query_db "$dbname" "INSERT INTO users (name) VALUES ('After Rotation');"
608 # Give it time to flush
612 kill -INT "$pg_scribe_pid" 2>/dev/null || true
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
619 wait "$pg_scribe_pid" 2>/dev/null || true
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"
627 if grep -q "After Rotation" "$rotated_file"; then
628 log_fail "Old file should not contain data after rotation"
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"
638 if ! grep -q "After Rotation" "$output_file"; then
639 log_fail "New file missing data after rotation"
643 if grep -q "Before Rotation" "$output_file"; then
644 log_fail "New file should not contain data before rotation"
648 log_pass "Log rotation with SIGHUP works correctly"
656 # shellcheck disable=SC2317 # Function called via trap handler
658 log_info "Cleaning up test resources..."
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
667 # Wait briefly for graceful shutdown
670 while kill -0 "$pid" 2>/dev/null && [[ $count -lt $timeout ]]; do
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
683 # Wait for child pg_recvlogical processes to fully terminate
684 # (They may take a moment to shut down after parent terminates)
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
695 for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
696 drop_test_db "$dbname"
699 # Remove test directory
700 if [[ -d "$TEST_DIR" ]]; then
704 log_info "Cleanup complete"
712 echo "========================================"
713 echo "pg_scribe --start Test Suite"
714 echo "========================================"
717 # Verify pg_scribe exists
718 if [[ ! -x "$PG_SCRIBE" ]]; then
719 echo "ERROR: pg_scribe not found or not executable: $PG_SCRIBE"
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"
729 # Verify wal_level is logical
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"
738 # Verify wal2sql extension is available
739 if ! pg_config --pkglibdir &>/dev/null; then
740 echo "ERROR: pg_config not found"
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"
752 # Create test directory
755 # Set up cleanup trap
756 trap cleanup EXIT INT TERM
758 echo "Running tests..."
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
773 echo "========================================"
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}"
781 if [[ $TESTS_FAILED -eq 0 ]]; then
782 echo -e "${GREEN}All tests passed!${NC}"
785 echo -e "${RED}Some tests failed!${NC}"