3 # Test suite for pg_scribe --status command
6 # - Creates temporary test databases
7 # - Tests various --status scenarios
8 # - Verifies status reporting and health checks
9 # - Cleans up all resources
14 # Colors for test output
19 NC='\033[0m' # No Color
22 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
23 PG_SCRIBE="$SCRIPT_DIR/scripts/pg_scribe"
24 TEST_DIR="/tmp/pg_scribe_test_status_$$"
25 TEST_DB_PREFIX="pg_scribe_test_status_$$"
26 PGUSER="${PGUSER:-postgres}"
34 DATABASES_TO_CLEANUP=()
42 echo -e "${BLUE}TEST:${NC} $*"
46 echo -e "${GREEN}PASS:${NC} $*"
51 echo -e "${RED}FAIL:${NC} $*"
56 echo -e "${YELLOW}INFO:${NC} $*"
66 psql -U "$PGUSER" -d "$dbname" -tAq "$@"
72 run_psql "$dbname" -c "$query" 2>/dev/null || true
77 log_info "Creating test database: $dbname"
80 psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $dbname;" &>/dev/null || true
83 psql -U "$PGUSER" -d postgres -c "CREATE DATABASE $dbname;" &>/dev/null
85 DATABASES_TO_CLEANUP+=("$dbname")
88 # shellcheck disable=SC2317 # Function called from cleanup trap handler
91 log_info "Dropping test database: $dbname"
93 # Terminate connections
94 psql -U "$PGUSER" -d postgres -c "
95 SELECT pg_terminate_backend(pid)
97 WHERE datname = '$dbname' AND pid <> pg_backend_pid();
101 psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $dbname;" &>/dev/null || true
104 # shellcheck disable=SC2317 # Function called from cleanup trap handler
105 drop_replication_slot() {
108 log_info "Dropping replication slot: $slot"
110 # Check if slot exists
112 exists=$(query_db "$dbname" "
113 SELECT 1 FROM pg_replication_slots WHERE slot_name = '$slot';
116 if [[ -n "$exists" ]]; then
118 query_db "$dbname" "SELECT pg_drop_replication_slot('$slot');" || true
122 create_table_with_pk() {
126 CREATE TABLE $table (
127 id SERIAL PRIMARY KEY,
129 created_at TIMESTAMP DEFAULT now()
134 initialize_backup_system() {
137 local backup_dir="$3"
139 # Create backup directory
140 mkdir -p "$backup_dir"
142 # Initialize (exit on failure - this is critical for tests)
144 init_output=$("$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" 2>&1)
147 if [[ $init_exit -ne 0 && $init_exit -ne 10 ]]; then
148 log_info "ERROR: Failed to initialize backup system for $dbname (exit=$init_exit)"
149 echo "$init_output" >&2
159 test_status_without_slot() {
161 log_test "Status check when slot doesn't exist (should fail)"
163 local dbname="${TEST_DB_PREFIX}_noslot"
164 local slot="test_slot_noslot"
166 # Setup - create db but DON'T initialize
167 create_test_db "$dbname"
168 create_table_with_pk "$dbname" "users"
170 # Try to check status - should fail with exit code 3 (slot error)
172 "$PG_SCRIBE" --status -d "$dbname" -S "$slot" -U "$PGUSER" &>/dev/null || exit_code=$?
174 if [[ $exit_code -eq 3 ]]; then
175 log_pass "Correctly failed with slot error"
178 log_fail "Expected exit code 3, got $exit_code"
183 test_status_basic_healthy() {
185 log_test "Basic status check (without backup directory)"
187 local dbname="${TEST_DB_PREFIX}_healthy"
188 local slot="test_slot_healthy"
189 local backup_dir="$TEST_DIR/healthy"
192 create_test_db "$dbname"
193 create_table_with_pk "$dbname" "users"
194 initialize_backup_system "$dbname" "$slot" "$backup_dir"
196 # Check status WITHOUT backup directory - should succeed
197 # (warnings about inactive slot, but exit code 10 is acceptable)
198 local output_file="$TEST_DIR/healthy_output.txt"
200 "$PG_SCRIBE" --status -d "$dbname" -S "$slot" -U "$PGUSER" &>"$output_file" || exit_code=$?
202 # Exit code 0 or 10 (warning about inactive slot) are acceptable
203 if [[ $exit_code -ne 0 && $exit_code -ne 10 ]]; then
204 log_fail "Status check failed with unexpected exit code $exit_code"
208 # Verify output contains expected information
209 if ! grep -q "Slot Name:" "$output_file"; then
210 log_fail "Output missing slot name"
214 if ! grep -q "Active:" "$output_file"; then
215 log_fail "Output missing active status"
219 if ! grep -q "Restart Lag:" "$output_file"; then
220 log_fail "Output missing lag information"
224 log_pass "Status check successful"
228 test_status_with_backup_directory() {
230 log_test "Status check with chain inventory"
232 local dbname="${TEST_DB_PREFIX}_with_dir"
233 local slot="test_slot_with_dir"
234 local backup_dir="$TEST_DIR/with_dir"
237 create_test_db "$dbname"
238 create_table_with_pk "$dbname" "users"
239 initialize_backup_system "$dbname" "$slot" "$backup_dir"
241 # Check status with backup directory
242 local output_file="$TEST_DIR/with_dir_output.txt"
244 "$PG_SCRIBE" --status -d "$dbname" -S "$slot" -f "$backup_dir" -U "$PGUSER" &>"$output_file" || exit_code=$?
246 # Exit code 0 or 10 (warning) are both acceptable here
247 # (warning because slot is not active)
248 if [[ $exit_code -ne 0 && $exit_code -ne 10 ]]; then
249 log_fail "Status check failed with unexpected exit code $exit_code"
253 # Verify chain inventory appears in output
254 if ! grep -q "Chain Inventory" "$output_file"; then
255 log_fail "Output missing chain inventory"
259 # Should show at least one chain
260 if ! grep -q "chain-" "$output_file"; then
261 log_fail "Output missing chain listing"
265 # Should show base backup size
266 if ! grep -q "Base backup:" "$output_file"; then
267 log_fail "Output missing base backup size"
271 # Should show differentials count
272 if ! grep -q "Differentials:" "$output_file"; then
273 log_fail "Output missing differentials count"
277 # Should show total size
278 if ! grep -q "Total size:" "$output_file"; then
279 log_fail "Output missing total size"
283 log_pass "Chain inventory works correctly"
287 test_status_inactive_slot() {
289 log_test "Status check with inactive slot (should warn)"
291 local dbname="${TEST_DB_PREFIX}_inactive"
292 local slot="test_slot_inactive"
293 local backup_dir="$TEST_DIR/inactive"
296 create_test_db "$dbname"
297 create_table_with_pk "$dbname" "users"
298 initialize_backup_system "$dbname" "$slot" "$backup_dir"
300 # Slot exists but is inactive (no pg_recvlogical running)
301 # Check status - should succeed but with warning
302 local output_file="$TEST_DIR/inactive_output.txt"
304 "$PG_SCRIBE" --status -d "$dbname" -S "$slot" -U "$PGUSER" &>"$output_file" || exit_code=$?
306 # Should exit with warning code (10)
307 if [[ $exit_code -ne 10 ]]; then
308 log_fail "Expected exit code 10 (warning), got $exit_code"
312 # Should mention inactive status
313 if ! grep -qi "not active" "$output_file"; then
314 log_fail "Output should mention inactive slot"
318 log_pass "Inactive slot warning works correctly"
322 test_status_with_active_streaming() {
324 log_test "Status check with active streaming"
326 local dbname="${TEST_DB_PREFIX}_streaming"
327 local slot="test_slot_streaming"
328 local backup_dir="$TEST_DIR/streaming"
331 create_test_db "$dbname"
332 create_table_with_pk "$dbname" "users"
333 initialize_backup_system "$dbname" "$slot" "$backup_dir"
335 # Start streaming in background (use backup directory for new chain system)
336 "$PG_SCRIBE" --start -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" -s 1 -F 1 &>/dev/null &
337 local pg_scribe_pid=$!
338 PIDS_TO_CLEANUP+=("$pg_scribe_pid")
340 # Wait for streaming to become active (with retry loop)
343 local slot_active=false
345 while [[ $retries -lt $max_retries ]]; do
347 local status_output="$TEST_DIR/streaming_status_retry_$retries.txt"
348 "$PG_SCRIBE" --status -d "$dbname" -S "$slot" -U "$PGUSER" &>"$status_output" || true
350 if grep -q "Active:.*Yes" "$status_output"; then
358 local status_output="$TEST_DIR/streaming_status.txt"
360 "$PG_SCRIBE" --status -d "$dbname" -S "$slot" -U "$PGUSER" &>"$status_output" || exit_code=$?
363 kill -INT "$pg_scribe_pid" 2>/dev/null || true
366 # Force kill if still running
367 if kill -0 "$pg_scribe_pid" 2>/dev/null; then
368 kill -9 "$pg_scribe_pid" 2>/dev/null || true
370 wait "$pg_scribe_pid" 2>/dev/null || true
372 # Verify status showed active slot
373 if [[ "$slot_active" != "true" ]]; then
374 log_fail "Slot should be shown as active (checked $retries times)"
375 cat "$status_output" >&2
379 log_pass "Active slot detected correctly"
383 test_status_with_incremental_backups() {
385 log_test "Status check with active streaming chain"
387 local dbname="${TEST_DB_PREFIX}_with_inc"
388 local slot="test_slot_with_inc"
389 local backup_dir="$TEST_DIR/with_inc"
392 create_test_db "$dbname"
393 create_table_with_pk "$dbname" "users"
394 initialize_backup_system "$dbname" "$slot" "$backup_dir"
396 # Start streaming to create active.sql
397 "$PG_SCRIBE" --start -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" -s 1 -F 1 &>/dev/null &
398 local pg_scribe_pid=$!
399 PIDS_TO_CLEANUP+=("$pg_scribe_pid")
401 # Give it a moment to start
404 # Make some changes to generate incremental data
405 query_db "$dbname" "INSERT INTO users (name) VALUES ('Test User');"
407 # Give it time to flush
410 # Check status with backup directory (while streaming is active)
411 local status_output="$TEST_DIR/with_inc_status.txt"
413 "$PG_SCRIBE" --status -d "$dbname" -S "$slot" -f "$backup_dir" -U "$PGUSER" &>"$status_output" || exit_code=$?
416 kill -INT "$pg_scribe_pid" 2>/dev/null || true
419 # Force kill if still running
420 if kill -0 "$pg_scribe_pid" 2>/dev/null; then
421 kill -9 "$pg_scribe_pid" 2>/dev/null || true
423 wait "$pg_scribe_pid" 2>/dev/null || true
425 # Should succeed (exit code 0)
426 if [[ $exit_code -ne 0 ]]; then
427 log_fail "Status check failed with unexpected exit code $exit_code"
428 cat "$status_output" >&2
432 # Verify chain shows as active
433 if ! grep -q "ACTIVE - streaming" "$status_output"; then
434 log_fail "Output should show active streaming chain"
435 cat "$status_output" >&2
440 if ! grep -q "PID:" "$status_output"; then
441 log_fail "Output should show streaming PID"
442 cat "$status_output" >&2
446 # Should show last activity
447 if ! grep -q "Last activity:" "$status_output"; then
448 log_fail "Output should show last activity timestamp"
449 cat "$status_output" >&2
453 log_pass "Active streaming chain reported correctly"
457 test_status_with_sealed_differentials() {
459 log_test "Status check with sealed differential files"
461 local dbname="${TEST_DB_PREFIX}_with_diffs"
462 local slot="test_slot_with_diffs"
463 local backup_dir="$TEST_DIR/with_diffs"
466 create_test_db "$dbname"
467 create_table_with_pk "$dbname" "users"
468 initialize_backup_system "$dbname" "$slot" "$backup_dir"
470 # Find the chain directory
472 chain_dir=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | head -1)
474 if [[ -z "$chain_dir" ]]; then
475 log_fail "No chain directory found"
479 # Create mock sealed differential files
480 touch "$chain_dir/diff-20251018T120000Z.sql"
481 touch "$chain_dir/diff-20251018T130000Z.sql"
482 touch "$chain_dir/diff-20251018T140000Z.sql"
485 local output_file="$TEST_DIR/with_diffs_output.txt"
487 "$PG_SCRIBE" --status -d "$dbname" -S "$slot" -f "$backup_dir" -U "$PGUSER" &>"$output_file" || exit_code=$?
489 # Should succeed with warning (inactive slot)
490 if [[ $exit_code -ne 0 && $exit_code -ne 10 ]]; then
491 log_fail "Status check failed with unexpected exit code $exit_code"
492 cat "$output_file" >&2
496 # Should show 3 sealed differentials
497 if ! grep -q "Differentials:.*3 sealed" "$output_file"; then
498 log_fail "Should report 3 sealed differentials"
499 cat "$output_file" >&2
503 log_pass "Sealed differentials counted correctly"
507 test_status_nonexistent_backup_dir() {
509 log_test "Status check with nonexistent backup directory"
511 local dbname="${TEST_DB_PREFIX}_no_dir"
512 local slot="test_slot_no_dir"
513 local backup_dir="$TEST_DIR/no_dir"
514 local fake_dir="$TEST_DIR/does_not_exist"
516 # Setup - initialize but check status with different directory
517 create_test_db "$dbname"
518 create_table_with_pk "$dbname" "users"
519 initialize_backup_system "$dbname" "$slot" "$backup_dir"
521 # Check status with nonexistent directory
522 local output_file="$TEST_DIR/no_dir_output.txt"
524 "$PG_SCRIBE" --status -d "$dbname" -S "$slot" -f "$fake_dir" -U "$PGUSER" &>"$output_file" || exit_code=$?
526 # Should exit with warning (10)
527 if [[ $exit_code -ne 10 ]]; then
528 log_fail "Expected exit code 10 (warning), got $exit_code"
532 # Should warn about nonexistent directory
533 if ! grep -qi "does not exist" "$output_file"; then
534 log_fail "Should warn about nonexistent directory"
538 log_pass "Nonexistent directory warning works correctly"
542 test_status_lag_display() {
544 log_test "Status displays lag information correctly"
546 local dbname="${TEST_DB_PREFIX}_lag"
547 local slot="test_slot_lag"
548 local backup_dir="$TEST_DIR/lag"
551 create_test_db "$dbname"
552 create_table_with_pk "$dbname" "users"
554 if ! initialize_backup_system "$dbname" "$slot" "$backup_dir"; then
555 log_fail "Failed to initialize backup system"
560 local output_file="$TEST_DIR/lag_output.txt"
561 "$PG_SCRIBE" --status -d "$dbname" -S "$slot" -U "$PGUSER" &>"$output_file" || true
563 # Verify lag is displayed - use case-insensitive grep for ANSI color codes
564 if ! grep -i "restart.*lag" "$output_file"; then
565 log_fail "Output missing restart lag"
566 cat "$output_file" >&2
570 if ! grep -i "confirmed.*lag" "$output_file"; then
571 log_fail "Output missing confirmed lag"
572 cat "$output_file" >&2
576 # Verify lag values are in MB
577 if ! grep "MB" "$output_file"; then
578 log_fail "Lag should be displayed in MB"
579 cat "$output_file" >&2
583 log_pass "Lag information displayed correctly"
587 test_status_multiple_chains() {
589 log_test "Status with multiple chains shows all"
591 local dbname="${TEST_DB_PREFIX}_multi_chains"
592 local slot="test_slot_multi_chains"
593 local backup_dir="$TEST_DIR/multi_chains"
596 create_test_db "$dbname"
597 create_table_with_pk "$dbname" "users"
599 if ! initialize_backup_system "$dbname" "$slot" "$backup_dir"; then
600 log_fail "Failed to initialize backup system"
604 # Create additional chain directories manually
605 mkdir -p "$backup_dir/chain-20250101T120000Z"
606 touch "$backup_dir/chain-20250101T120000Z/base.sql"
607 touch "$backup_dir/chain-20250101T120000Z/globals.sql"
608 echo '{}' > "$backup_dir/chain-20250101T120000Z/metadata.json"
610 mkdir -p "$backup_dir/chain-20250102T120000Z"
611 touch "$backup_dir/chain-20250102T120000Z/base.sql"
612 touch "$backup_dir/chain-20250102T120000Z/globals.sql"
613 echo '{}' > "$backup_dir/chain-20250102T120000Z/metadata.json"
616 local output_file="$TEST_DIR/multi_chains_output.txt"
617 "$PG_SCRIBE" --status -d "$dbname" -S "$slot" -f "$backup_dir" -U "$PGUSER" &>"$output_file" || true
619 # Count chains displayed - should be 3
621 chain_count=$(grep -c "chain-" "$output_file" || echo 0)
623 if [[ $chain_count -lt 3 ]]; then
624 log_fail "Expected at least 3 chains in output, found $chain_count"
625 cat "$output_file" >&2
629 # Verify chains are sorted (check that chain-20250101 comes before chain-20250102)
630 if ! grep -A5 "chain-20250101" "$output_file" | grep -q "chain-20250102"; then
631 log_fail "Chains should be displayed in sorted order"
632 cat "$output_file" >&2
636 log_pass "Multiple chains displayed correctly"
644 # shellcheck disable=SC2317 # Function called via trap handler
646 log_info "Cleaning up test resources..."
648 # Kill any running pg_scribe processes
649 for pid in "${PIDS_TO_CLEANUP[@]}"; do
650 if kill -0 "$pid" 2>/dev/null; then
651 log_info "Stopping pg_scribe process $pid"
652 # Try graceful shutdown first (allows signal forwarding to child processes)
653 kill -TERM "$pid" 2>/dev/null || true
655 # Wait briefly for graceful shutdown
658 while kill -0 "$pid" 2>/dev/null && [[ $count -lt $timeout ]]; do
663 # Force kill if still running
664 if kill -0 "$pid" 2>/dev/null; then
665 log_info "Force killing pg_scribe process $pid"
666 kill -9 "$pid" 2>/dev/null || true
671 # Wait for child pg_recvlogical processes to fully terminate
672 # (They may take a moment to shut down after parent terminates)
675 # Drop replication slots
676 for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
677 for slot in test_slot_noslot test_slot_healthy test_slot_with_dir test_slot_inactive test_slot_streaming test_slot_with_inc test_slot_with_diffs test_slot_no_dir test_slot_lag test_slot_multi_chains; do
678 drop_replication_slot "$dbname" "$slot" 2>/dev/null || true
683 for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
684 drop_test_db "$dbname"
687 # Remove test directory
688 if [[ -d "$TEST_DIR" ]]; then
692 log_info "Cleanup complete"
700 echo "========================================"
701 echo "pg_scribe --status Test Suite"
702 echo "========================================"
705 # Verify pg_scribe exists
706 if [[ ! -x "$PG_SCRIBE" ]]; then
707 echo "ERROR: pg_scribe not found or not executable: $PG_SCRIBE"
711 # Verify PostgreSQL is running
712 if ! psql -U "$PGUSER" -d postgres -c "SELECT 1;" &>/dev/null; then
713 echo "ERROR: Cannot connect to PostgreSQL"
717 # Verify wal_level is logical
719 wal_level=$(psql -U "$PGUSER" -d postgres -tAq -c "SHOW wal_level;")
720 if [[ "$wal_level" != "logical" ]]; then
721 echo "ERROR: wal_level must be 'logical', currently: $wal_level"
722 echo "Update ~/.pgenv/pgsql/data/postgresql.conf and restart PostgreSQL"
726 # Create test directory
729 # Set up cleanup trap
730 trap cleanup EXIT INT TERM
732 echo "Running tests..."
735 # Run all tests (use || true to prevent set -e from exiting)
736 test_status_without_slot || true
737 test_status_basic_healthy || true
738 test_status_with_backup_directory || true
739 test_status_inactive_slot || true
740 test_status_with_active_streaming || true
741 test_status_with_incremental_backups || true
742 test_status_with_sealed_differentials || true
743 test_status_nonexistent_backup_dir || true
744 test_status_lag_display || true
745 test_status_multiple_chains || true
749 echo "========================================"
751 echo "========================================"
752 echo "Tests run: $TESTS_RUN"
753 echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}"
754 echo -e "Tests failed: ${RED}$TESTS_FAILED${NC}"
757 if [[ $TESTS_FAILED -eq 0 ]]; then
758 echo -e "${GREEN}All tests passed!${NC}"
761 echo -e "${RED}Some tests failed!${NC}"