]> begriffs open source - pg_scribe/blob - tests/test_stop.sh
Better way to make a new chain and transfer streaming to it
[pg_scribe] / tests / test_stop.sh
1 #!/usr/bin/env bash
2 #
3 # Test suite for pg_scribe --stop command
4 #
5 # This test suite:
6 # - Creates temporary test databases
7 # - Tests various --stop scenarios
8 # - Verifies expected outcomes
9 # - Cleans up all resources
10 #
11
12 set -euo pipefail
13
14 # Colors for test output
15 RED='\033[0;31m'
16 GREEN='\033[0;32m'
17 YELLOW='\033[0;33m'
18 BLUE='\033[0;34m'
19 NC='\033[0m' # No Color
20
21 # Test configuration
22 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
23 PG_SCRIBE="$SCRIPT_DIR/scripts/pg_scribe"
24 TEST_DIR="/tmp/pg_scribe_test_$$"
25 TEST_DB_PREFIX="pg_scribe_test_$$"
26 PGUSER="${PGUSER:-postgres}"
27
28 # Test counters
29 TESTS_RUN=0
30 TESTS_PASSED=0
31 TESTS_FAILED=0
32
33 # Cleanup tracking
34 DATABASES_TO_CLEANUP=()
35
36 #
37 # Logging functions
38 #
39
40 log_test() {
41     echo -e "${BLUE}TEST:${NC} $*"
42 }
43
44 log_pass() {
45     echo -e "${GREEN}PASS:${NC} $*"
46     ((TESTS_PASSED++))
47 }
48
49 log_fail() {
50     echo -e "${RED}FAIL:${NC} $*"
51     ((TESTS_FAILED++))
52 }
53
54 log_info() {
55     echo -e "${YELLOW}INFO:${NC} $*"
56 }
57
58 #
59 # Helper functions
60 #
61
62 run_psql() {
63     local dbname="$1"
64     shift
65     psql -U "$PGUSER" -d "$dbname" -tAq "$@"
66 }
67
68 query_db() {
69     local dbname="$1"
70     local query="$2"
71     run_psql "$dbname" -c "$query" 2>/dev/null || true
72 }
73
74 create_test_db() {
75     local dbname="$1"
76     log_info "Creating test database: $dbname"
77
78     # Drop if exists
79     psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $dbname;" &>/dev/null || true
80
81     # Create database
82     psql -U "$PGUSER" -d postgres -c "CREATE DATABASE $dbname;" &>/dev/null
83
84     DATABASES_TO_CLEANUP+=("$dbname")
85 }
86
87 # shellcheck disable=SC2317  # Function called from cleanup trap handler
88 drop_test_db() {
89     local dbname="$1"
90     log_info "Dropping test database: $dbname"
91
92     # Terminate connections
93     psql -U "$PGUSER" -d postgres -c "
94         SELECT pg_terminate_backend(pid)
95         FROM pg_stat_activity
96         WHERE datname = '$dbname' AND pid <> pg_backend_pid();
97     " &>/dev/null || true
98
99     # Drop database
100     psql -U "$PGUSER" -d postgres -c "DROP DATABASE IF EXISTS $dbname;" &>/dev/null || true
101 }
102
103 # shellcheck disable=SC2317  # Function called from cleanup trap handler
104 drop_replication_slot() {
105     local dbname="$1"
106     local slot="$2"
107     log_info "Dropping replication slot: $slot"
108
109     # Check if slot exists
110     local exists
111     exists=$(query_db "$dbname" "
112         SELECT 1 FROM pg_replication_slots WHERE slot_name = '$slot';
113     ")
114
115     if [[ -n "$exists" ]]; then
116         # Drop slot
117         query_db "$dbname" "SELECT pg_drop_replication_slot('$slot');" || true
118     fi
119 }
120
121 create_table_with_pk() {
122     local dbname="$1"
123     local table="$2"
124     query_db "$dbname" "
125         CREATE TABLE $table (
126             id SERIAL PRIMARY KEY,
127             name TEXT,
128             created_at TIMESTAMP DEFAULT now()
129         );
130     "
131 }
132
133 # Initialize a backup directory (creates replication slot and initial backups)
134 init_backup_system() {
135     local dbname="$1"
136     local backup_dir="$2"
137     local slot="$3"
138
139     mkdir -p "$backup_dir"
140     "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null
141 }
142
143 # Start streaming in the background
144 start_streaming_background() {
145     local dbname="$1"
146     local backup_dir="$2"
147     local slot="$3"
148
149     "$PG_SCRIBE" --start -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null &
150     local pid=$!
151
152     # Wait for pidfile to appear (instead of fixed sleep)
153     local pidfile="$backup_dir/.pg_scribe.pid"
154     local timeout=10
155     local waited=0
156
157     while [[ ! -f "$pidfile" ]] && [[ $waited -lt $timeout ]]; do
158         sleep 0.5
159         waited=$((waited + 1))
160     done
161
162     # Verify process is still running
163     if ! kill -0 "$pid" 2>/dev/null; then
164         return 1
165     fi
166
167     # Verify pidfile was created
168     if [[ ! -f "$pidfile" ]]; then
169         return 1
170     fi
171
172     echo "$pid"
173 }
174
175 #
176 # Test cases
177 #
178
179 test_stop_requires_backup_dir() {
180     ((TESTS_RUN++))
181     log_test "Stop requires backup directory"
182
183     local exit_code=0
184     "$PG_SCRIBE" --stop &>/dev/null || exit_code=$?
185
186     if [[ $exit_code -eq 5 ]]; then
187         log_pass "Correctly rejects missing backup directory"
188         return 0
189     else
190         log_fail "Expected exit code 5, got $exit_code"
191         return 1
192     fi
193 }
194
195 test_stop_nonexistent_directory() {
196     ((TESTS_RUN++))
197     log_test "Stop fails with nonexistent directory"
198
199     local backup_dir="$TEST_DIR/nonexistent"
200
201     local exit_code=0
202     "$PG_SCRIBE" --stop -f "$backup_dir" -U "$PGUSER" &>/dev/null || exit_code=$?
203
204     if [[ $exit_code -eq 4 ]]; then
205         log_pass "Correctly rejects nonexistent directory"
206         return 0
207     else
208         log_fail "Expected exit code 4, got $exit_code"
209         return 1
210     fi
211 }
212
213 test_stop_no_active_process() {
214     ((TESTS_RUN++))
215     log_test "Stop fails when no active process"
216
217     local dbname="${TEST_DB_PREFIX}_noprocess"
218     local backup_dir="$TEST_DIR/noprocess"
219     local slot="test_slot_noprocess"
220
221     # Setup - initialize but don't start
222     create_test_db "$dbname"
223     create_table_with_pk "$dbname" "users"
224     init_backup_system "$dbname" "$backup_dir" "$slot"
225
226     # Try to stop (should fail - no process running)
227     local exit_code=0
228     "$PG_SCRIBE" --stop -f "$backup_dir" -U "$PGUSER" &>/dev/null || exit_code=$?
229
230     if [[ $exit_code -eq 1 ]]; then
231         log_pass "Correctly fails when no process running"
232         return 0
233     else
234         log_fail "Expected exit code 1, got $exit_code"
235         return 1
236     fi
237 }
238
239 test_stop_active_process() {
240     ((TESTS_RUN++))
241     log_test "Stop successfully stops active streaming"
242
243     local dbname="${TEST_DB_PREFIX}_active"
244     local backup_dir="$TEST_DIR/active"
245     local slot="test_slot_active"
246
247     # Setup - initialize and start streaming
248     create_test_db "$dbname"
249     create_table_with_pk "$dbname" "users"
250     init_backup_system "$dbname" "$backup_dir" "$slot"
251
252     # Start streaming in background
253     local pid
254     pid=$(start_streaming_background "$dbname" "$backup_dir" "$slot")
255
256     if [[ -z "$pid" ]]; then
257         log_fail "Failed to start streaming"
258         return 1
259     fi
260
261     # Stop the process
262     if "$PG_SCRIBE" --stop -f "$backup_dir" -U "$PGUSER" &>/dev/null; then
263         # Verify process is actually stopped
264         sleep 1
265         if kill -0 "$pid" 2>/dev/null; then
266             log_fail "Process still running after stop"
267             kill -KILL "$pid" 2>/dev/null || true
268             return 1
269         fi
270
271         # Verify pidfile was removed
272         local pidfile="$backup_dir/.pg_scribe.pid"
273         if [[ -f "$pidfile" ]]; then
274             log_fail "Pidfile not removed"
275             return 1
276         fi
277
278         log_pass "Successfully stopped active streaming"
279         return 0
280     else
281         log_fail "Stop command failed"
282         kill -TERM "$pid" 2>/dev/null || true
283         return 1
284     fi
285 }
286
287 test_stop_stale_pidfile() {
288     ((TESTS_RUN++))
289     log_test "Stop handles stale pidfile gracefully"
290
291     local dbname="${TEST_DB_PREFIX}_stale"
292     local backup_dir="$TEST_DIR/stale"
293     local slot="test_slot_stale"
294
295     # Setup
296     create_test_db "$dbname"
297     create_table_with_pk "$dbname" "users"
298     init_backup_system "$dbname" "$backup_dir" "$slot"
299
300     # Create stale pidfile (with non-existent PID)
301     local pidfile="$backup_dir/.pg_scribe.pid"
302     echo "99999" > "$pidfile"
303
304     # Stop should handle this gracefully (exit 0 and remove stale pidfile)
305     if "$PG_SCRIBE" --stop -f "$backup_dir" -U "$PGUSER" &>/dev/null; then
306         # Verify pidfile was removed
307         if [[ -f "$pidfile" ]]; then
308             log_fail "Stale pidfile not removed"
309             return 1
310         fi
311
312         log_pass "Stale pidfile handled gracefully"
313         return 0
314     else
315         log_fail "Stop failed on stale pidfile"
316         return 1
317     fi
318 }
319
320 #
321 # Cleanup
322 #
323
324 # shellcheck disable=SC2317  # Function called via trap handler
325 cleanup() {
326     log_info "Cleaning up test resources..."
327
328     # Kill any remaining pg_recvlogical processes
329     pkill -f "pg_recvlogical.*test_slot" 2>/dev/null || true
330     sleep 1
331
332     # Drop replication slots
333     for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
334         for slot in test_slot_noprocess test_slot_active test_slot_stale; do
335             drop_replication_slot "$dbname" "$slot" 2>/dev/null || true
336         done
337     done
338
339     # Drop databases
340     for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
341         drop_test_db "$dbname"
342     done
343
344     # Remove test directory
345     if [[ -d "$TEST_DIR" ]]; then
346         rm -rf "$TEST_DIR"
347     fi
348
349     log_info "Cleanup complete"
350 }
351
352 #
353 # Main test runner
354 #
355
356 main() {
357     echo "========================================"
358     echo "pg_scribe --stop Test Suite"
359     echo "========================================"
360     echo ""
361
362     # Verify pg_scribe exists
363     if [[ ! -x "$PG_SCRIBE" ]]; then
364         echo "ERROR: pg_scribe not found or not executable: $PG_SCRIBE"
365         exit 1
366     fi
367
368     # Verify PostgreSQL is running
369     if ! psql -U "$PGUSER" -d postgres -c "SELECT 1;" &>/dev/null; then
370         echo "ERROR: Cannot connect to PostgreSQL"
371         exit 1
372     fi
373
374     # Verify wal_level is logical
375     local wal_level
376     wal_level=$(psql -U "$PGUSER" -d postgres -tAq -c "SHOW wal_level;")
377     if [[ "$wal_level" != "logical" ]]; then
378         echo "ERROR: wal_level must be 'logical', currently: $wal_level"
379         echo "Update ~/.pgenv/pgsql/data/postgresql.conf and restart PostgreSQL"
380         exit 1
381     fi
382
383     # Create test directory
384     mkdir -p "$TEST_DIR"
385
386     # Set up cleanup trap
387     trap cleanup EXIT INT TERM
388
389     echo "Running tests..."
390     echo ""
391
392     # Run all tests (use || true to prevent set -e from exiting)
393     test_stop_requires_backup_dir || true
394     test_stop_nonexistent_directory || true
395     test_stop_no_active_process || true
396     test_stop_active_process || true
397     test_stop_stale_pidfile || true
398
399     # Summary
400     echo ""
401     echo "========================================"
402     echo "Test Results"
403     echo "========================================"
404     echo "Tests run:    $TESTS_RUN"
405     echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}"
406     echo -e "Tests failed: ${RED}$TESTS_FAILED${NC}"
407     echo ""
408
409     if [[ $TESTS_FAILED -eq 0 ]]; then
410         echo -e "${GREEN}All tests passed!${NC}"
411         exit 0
412     else
413         echo -e "${RED}Some tests failed!${NC}"
414         exit 1
415     fi
416 }
417
418 main "$@"