]> begriffs open source - pg_scribe/blob - tests/test_new_chain.sh
Apply diffs faster with synchronous_commit = off
[pg_scribe] / tests / test_new_chain.sh
1 #!/usr/bin/env bash
2 #
3 # Test suite for pg_scribe --new-chain command
4 #
5 # This test suite:
6 # - Creates temporary test databases
7 # - Tests various --new-chain 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 create_table_with_pk() {
104     local dbname="$1"
105     local table="$2"
106     query_db "$dbname" "
107         CREATE TABLE $table (
108             id SERIAL PRIMARY KEY,
109             name TEXT,
110             created_at TIMESTAMP DEFAULT now()
111         );
112     "
113 }
114
115 # Initialize a backup directory (creates replication slot and initial backups)
116 init_backup_system() {
117     local dbname="$1"
118     local backup_dir="$2"
119     local slot="$3"
120
121     mkdir -p "$backup_dir"
122     "$PG_SCRIBE" --init -d "$dbname" -f "$backup_dir" -S "$slot" -U "$PGUSER" &>/dev/null
123 }
124
125 #
126 # Test cases
127 #
128
129 test_new_chain_requires_args() {
130     ((TESTS_RUN++))
131     log_test "New chain requires database and directory"
132
133     local exit_code=0
134
135     # Missing database
136     "$PG_SCRIBE" --new-chain -f /tmp/test &>/dev/null || exit_code=$?
137     if [[ $exit_code -ne 5 ]]; then
138         log_fail "Should fail with exit code 5 when missing database"
139         return 1
140     fi
141
142     # Missing directory
143     exit_code=0
144     "$PG_SCRIBE" --new-chain -d testdb &>/dev/null || exit_code=$?
145     if [[ $exit_code -ne 5 ]]; then
146         log_fail "Should fail with exit code 5 when missing directory"
147         return 1
148     fi
149
150     log_pass "Argument validation works"
151     return 0
152 }
153
154 test_new_chain_directory_must_exist() {
155     ((TESTS_RUN++))
156     log_test "New chain requires existing directory"
157
158     local dbname="${TEST_DB_PREFIX}_dircheck"
159     local backup_dir="$TEST_DIR/nonexistent_dir"
160
161     create_test_db "$dbname"
162     create_table_with_pk "$dbname" "users"
163
164     # Try to create chain in non-existent directory
165     local exit_code=0
166     "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -U "$PGUSER" &>/dev/null || exit_code=$?
167
168     if [[ $exit_code -eq 4 ]]; then
169         log_pass "Correctly rejects non-existent directory"
170         return 0
171     else
172         log_fail "Expected exit code 4, got $exit_code"
173         return 1
174     fi
175 }
176
177 test_new_chain_basic_success() {
178     ((TESTS_RUN++))
179     log_test "Basic new chain success (no compression)"
180
181     local dbname="${TEST_DB_PREFIX}_basic"
182     local backup_dir="$TEST_DIR/basic"
183     local slot="test_slot_basic"
184
185     # Setup - initialize backup system first
186     create_test_db "$dbname"
187     create_table_with_pk "$dbname" "users"
188     query_db "$dbname" "INSERT INTO users (name) VALUES ('Alice'), ('Bob'), ('Charlie');"
189     init_backup_system "$dbname" "$backup_dir" "$slot"
190
191     # Sleep to ensure different timestamp
192     sleep 2
193
194     # Create a new chain without compression
195     if "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -Z none -U "$PGUSER" &>/dev/null; then
196         # Count chain directories (should have 2: 1 from init, 1 from new-chain)
197         local chain_count
198         chain_count=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | wc -l)
199
200         if [[ $chain_count -ne 2 ]]; then
201             log_fail "Expected 2 chain directories, got $chain_count"
202             return 1
203         fi
204
205         # Get latest chain directory
206         local latest_chain
207         latest_chain=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | sort | tail -1)
208
209         # Verify chain structure
210         if [[ ! -f "$latest_chain/base.sql" ]]; then
211             log_fail "base.sql not found in latest chain"
212             return 1
213         fi
214
215         if [[ ! -f "$latest_chain/globals.sql" ]]; then
216             log_fail "globals.sql not found in latest chain"
217             return 1
218         fi
219
220         if [[ ! -f "$latest_chain/metadata.json" ]]; then
221             log_fail "metadata.json not found in latest chain"
222             return 1
223         fi
224
225         # Verify backup content
226         if ! grep -q "CREATE TABLE public.users" "$latest_chain/base.sql"; then
227             log_fail "Base backup missing table definition"
228             return 1
229         fi
230
231         if ! grep -q "Alice" "$latest_chain/base.sql"; then
232             log_fail "Base backup missing data"
233             return 1
234         fi
235
236         # Verify metadata content (JSON format)
237         if ! grep -q "\"database\": \"$dbname\"" "$latest_chain/metadata.json"; then
238             log_fail "Metadata missing database name"
239             return 1
240         fi
241
242         log_pass "Basic new chain successful"
243         return 0
244     else
245         log_fail "New chain command failed"
246         return 1
247     fi
248 }
249
250 test_new_chain_with_gzip_compression() {
251     ((TESTS_RUN++))
252     log_test "New chain with gzip compression"
253
254     local dbname="${TEST_DB_PREFIX}_gzip"
255     local backup_dir="$TEST_DIR/gzip"
256     local slot="test_slot_gzip"
257
258     # Setup
259     create_test_db "$dbname"
260     create_table_with_pk "$dbname" "data_table"
261     # Add enough data to see compression benefit
262     query_db "$dbname" "INSERT INTO data_table (name) SELECT 'Row ' || generate_series(1, 1000);"
263     init_backup_system "$dbname" "$backup_dir" "$slot"
264
265     # Sleep to ensure different timestamp
266     sleep 2
267
268     # Create new chain with gzip compression
269     if "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -Z gzip -U "$PGUSER" &>/dev/null; then
270         # Get latest chain directory
271         local latest_chain
272         latest_chain=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | sort | tail -1)
273
274         # Check for compressed base.sql in chain directory
275         if [[ ! -f "$latest_chain/base.sql.gz" ]]; then
276             log_fail "No gzip-compressed base backup found in chain"
277             return 1
278         fi
279
280         # Globals are not compressed (too small to benefit)
281         if [[ ! -f "$latest_chain/globals.sql" ]]; then
282             log_fail "No globals backup found in chain"
283             return 1
284         fi
285
286         # Verify we can decompress and read the backup
287         if ! gunzip -t "$latest_chain/base.sql.gz" &>/dev/null; then
288             log_fail "Compressed backup file is invalid"
289             return 1
290         fi
291
292         # Verify content
293         if ! gunzip -c "$latest_chain/base.sql.gz" | grep -q "CREATE TABLE public.data_table"; then
294             log_fail "Compressed backup missing table definition"
295             return 1
296         fi
297
298         log_pass "Gzip compression successful"
299         return 0
300     else
301         log_fail "New chain with gzip failed"
302         return 1
303     fi
304 }
305
306 test_new_chain_no_compression() {
307     ((TESTS_RUN++))
308     log_test "New chain with no compression (default)"
309
310     local dbname="${TEST_DB_PREFIX}_nocomp"
311     local backup_dir="$TEST_DIR/nocomp"
312     local slot="test_slot_nocomp"
313
314     # Setup
315     create_test_db "$dbname"
316     create_table_with_pk "$dbname" "data_table"
317     query_db "$dbname" "INSERT INTO data_table (name) SELECT 'Row ' || generate_series(1, 500);"
318     init_backup_system "$dbname" "$backup_dir" "$slot"
319
320     # Sleep to ensure different timestamp
321     sleep 2
322
323     # Create new chain with default compression (none)
324     if "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -U "$PGUSER" &>/dev/null; then
325         # Get latest chain directory
326         local latest_chain
327         latest_chain=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | sort | tail -1)
328
329         # Check for uncompressed base.sql in chain directory
330         if [[ ! -f "$latest_chain/base.sql" ]]; then
331             log_fail "No uncompressed base backup found in chain"
332             return 1
333         fi
334
335         # Globals are not compressed (too small to benefit)
336         if [[ ! -f "$latest_chain/globals.sql" ]]; then
337             log_fail "No globals backup found in chain"
338             return 1
339         fi
340
341         # Verify content
342         if ! grep -q "CREATE TABLE public.data_table" "$latest_chain/base.sql"; then
343             log_fail "Base backup missing table definition"
344             return 1
345         fi
346
347         log_pass "No compression (default) successful"
348         return 0
349     else
350         log_fail "New chain with default compression failed"
351         return 1
352     fi
353 }
354
355 test_new_chain_multiple_times() {
356     ((TESTS_RUN++))
357     log_test "Multiple new chains (retention simulation)"
358
359     local dbname="${TEST_DB_PREFIX}_multi"
360     local backup_dir="$TEST_DIR/multi"
361     local slot="test_slot_multi"
362
363     # Setup
364     create_test_db "$dbname"
365     create_table_with_pk "$dbname" "counter"
366     init_backup_system "$dbname" "$backup_dir" "$slot"
367
368     # Create multiple new chains with data changes
369     for i in 1 2 3; do
370         query_db "$dbname" "INSERT INTO counter (name) VALUES ('Iteration $i');"
371         sleep 1  # Ensure different timestamps
372
373         if ! "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -Z none -U "$PGUSER" &>/dev/null; then
374             log_fail "New chain $i failed"
375             return 1
376         fi
377     done
378
379     # Count total chains (1 from init + 3 from new-chain = 4)
380     local chain_count
381     chain_count=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | wc -l)
382
383     if [[ $chain_count -ne 4 ]]; then
384         log_fail "Expected 4 chain directories, got $chain_count"
385         return 1
386     fi
387
388     # Verify latest chain has all data
389     local latest_chain
390     latest_chain=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | sort | tail -1)
391
392     for i in 1 2 3; do
393         if ! grep -q "Iteration $i" "$latest_chain/base.sql"; then
394             log_fail "Latest chain missing data from iteration $i"
395             return 1
396         fi
397     done
398
399     log_pass "Multiple new chains successful"
400     return 0
401 }
402
403 test_new_chain_restorability() {
404     ((TESTS_RUN++))
405     log_test "New chain is restorable"
406
407     local dbname="${TEST_DB_PREFIX}_restore"
408     local backup_dir="$TEST_DIR/restore"
409     local slot="test_slot_restore"
410     local restore_dbname="${TEST_DB_PREFIX}_restored"
411
412     # Setup
413     create_test_db "$dbname"
414     create_table_with_pk "$dbname" "products"
415     query_db "$dbname" "INSERT INTO products (name) VALUES ('Widget'), ('Gadget'), ('Doohickey');"
416     init_backup_system "$dbname" "$backup_dir" "$slot"
417
418     # Create a new chain
419     if ! "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -Z none -U "$PGUSER" &>/dev/null; then
420         log_fail "New chain failed"
421         return 1
422     fi
423
424     # Use pg_scribe --restore to restore the chain
425     if ! "$PG_SCRIBE" --restore -d "$restore_dbname" -f "$backup_dir" -C -U "$PGUSER" &>/dev/null; then
426         log_fail "Restore failed"
427         return 1
428     fi
429
430     DATABASES_TO_CLEANUP+=("$restore_dbname")
431
432     # Verify restored data
433     local count
434     count=$(query_db "$restore_dbname" "SELECT COUNT(*) FROM products;")
435     if [[ "$count" -ne 3 ]]; then
436         log_fail "Expected 3 rows, got $count"
437         return 1
438     fi
439
440     local widget_exists
441     widget_exists=$(query_db "$restore_dbname" "SELECT COUNT(*) FROM products WHERE name = 'Widget';")
442     if [[ "$widget_exists" -ne 1 ]]; then
443         log_fail "Expected to find Widget in restored data"
444         return 1
445     fi
446
447     log_pass "New chain is restorable"
448     return 0
449 }
450
451 test_new_chain_with_complex_schema() {
452     ((TESTS_RUN++))
453     log_test "New chain with complex schema (indexes, constraints)"
454
455     local dbname="${TEST_DB_PREFIX}_complex"
456     local backup_dir="$TEST_DIR/complex"
457     local slot="test_slot_complex"
458
459     # Setup with complex schema
460     create_test_db "$dbname"
461
462     query_db "$dbname" "
463         CREATE TABLE authors (
464             id SERIAL PRIMARY KEY,
465             name TEXT NOT NULL UNIQUE,
466             email TEXT
467         );
468
469         CREATE TABLE books (
470             id SERIAL PRIMARY KEY,
471             title TEXT NOT NULL,
472             author_id INTEGER REFERENCES authors(id) ON DELETE CASCADE,
473             published_date DATE,
474             isbn TEXT UNIQUE
475         );
476
477         CREATE INDEX idx_books_title ON books(title);
478         CREATE INDEX idx_books_published ON books(published_date);
479     "
480
481     query_db "$dbname" "
482         INSERT INTO authors (name, email) VALUES ('John Doe', 'john@example.com');
483         INSERT INTO books (title, author_id, published_date, isbn)
484         VALUES ('Test Book', 1, '2024-01-01', '1234567890');
485     "
486
487     init_backup_system "$dbname" "$backup_dir" "$slot"
488
489     # Sleep to ensure different timestamp
490     sleep 2
491
492     # Create new chain
493     if ! "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -Z none -U "$PGUSER" &>/dev/null; then
494         log_fail "New chain failed"
495         return 1
496     fi
497
498     # Get latest chain and verify backup contains schema elements
499     local latest_chain
500     latest_chain=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | sort | tail -1)
501
502     if ! grep -q "CREATE TABLE public.authors" "$latest_chain/base.sql"; then
503         log_fail "Backup missing authors table"
504         return 1
505     fi
506
507     if ! grep -q "CREATE TABLE public.books" "$latest_chain/base.sql"; then
508         log_fail "Backup missing books table"
509         return 1
510     fi
511
512     if ! grep -q "UNIQUE" "$latest_chain/base.sql"; then
513         log_fail "Backup missing unique constraints"
514         return 1
515     fi
516
517     if ! grep -q "REFERENCES" "$latest_chain/base.sql"; then
518         log_fail "Backup missing foreign key"
519         return 1
520     fi
521
522     if ! grep -q "CREATE INDEX" "$latest_chain/base.sql"; then
523         log_fail "Backup missing indexes"
524         return 1
525     fi
526
527     log_pass "Complex schema backed up successfully"
528     return 0
529 }
530
531 test_new_chain_metadata_tracking() {
532     ((TESTS_RUN++))
533     log_test "Metadata file tracks chain information"
534
535     local dbname="${TEST_DB_PREFIX}_metadata"
536     local backup_dir="$TEST_DIR/metadata"
537     local slot="test_slot_metadata"
538
539     # Setup
540     create_test_db "$dbname"
541     create_table_with_pk "$dbname" "test_table"
542     init_backup_system "$dbname" "$backup_dir" "$slot"
543
544     # Sleep to ensure different timestamp
545     sleep 2
546
547     # Create a new chain
548     if ! "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -Z none -U "$PGUSER" &>/dev/null; then
549         log_fail "New chain failed"
550         return 1
551     fi
552
553     # Get latest chain and verify metadata content
554     local latest_chain
555     latest_chain=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | sort | tail -1)
556     local metadata_file="$latest_chain/metadata.json"
557
558     if [[ ! -f "$metadata_file" ]]; then
559         log_fail "Metadata file not found in chain"
560         return 1
561     fi
562
563     if ! grep -q "\"database\": \"$dbname\"" "$metadata_file"; then
564         log_fail "Metadata missing database name"
565         return 1
566     fi
567
568     if ! grep -q "\"created\":" "$metadata_file"; then
569         log_fail "Metadata missing created timestamp"
570         return 1
571     fi
572
573     log_pass "Metadata tracking works correctly"
574     return 0
575 }
576
577 test_new_chain_with_start_flag() {
578     ((TESTS_RUN++))
579     log_test "New chain with --start flag validates arguments"
580
581     local dbname="${TEST_DB_PREFIX}_start_flag"
582     local backup_dir="$TEST_DIR/start_flag"
583     local slot="test_slot_start_flag"
584
585     # Setup
586     create_test_db "$dbname"
587     create_table_with_pk "$dbname" "test_table"
588     init_backup_system "$dbname" "$backup_dir" "$slot"
589
590     # Test 1: --new-chain --start should require database name
591     local exit_code=0
592     "$PG_SCRIBE" --new-chain --start -f "$backup_dir" -U "$PGUSER" &>/dev/null || exit_code=$?
593
594     if [[ $exit_code -ne 5 ]]; then
595         log_fail "Should require database name with --start, got exit code $exit_code"
596         return 1
597     fi
598
599     # Test 2: Verify --start flag is accepted with proper arguments
600     # Note: We can't fully test the background behavior in a simple test,
601     # but we can verify the command parses correctly and would start
602     # (We won't actually let it start streaming to avoid complexity)
603
604     # Sleep to ensure different timestamp for chain
605     sleep 2
606
607     # Create new chain WITHOUT --start flag (to verify it works)
608     if ! "$PG_SCRIBE" --new-chain -d "$dbname" -f "$backup_dir" -Z none -U "$PGUSER" &>/dev/null; then
609         log_fail "New chain creation failed"
610         return 1
611     fi
612
613     # Verify 2 chains exist now
614     local chain_count
615     chain_count=$(find "$backup_dir" -maxdepth 1 -type d -name 'chain-*' 2>/dev/null | wc -l)
616
617     if [[ $chain_count -ne 2 ]]; then
618         log_fail "Expected 2 chains, got $chain_count"
619         return 1
620     fi
621
622     log_pass "--start flag argument parsing works correctly"
623     return 0
624 }
625
626 #
627 # Cleanup
628 #
629
630 # shellcheck disable=SC2317  # Function called via trap handler
631 cleanup() {
632     log_info "Cleaning up test resources..."
633
634     # Drop replication slots (before dropping databases)
635     for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
636         for slot in test_slot_basic test_slot_gzip test_slot_nocomp test_slot_multi test_slot_restore test_slot_complex test_slot_metadata test_slot_start_flag; do
637             # Try to drop slot
638             psql -U "$PGUSER" -d "$dbname" -c "
639                 SELECT pg_drop_replication_slot('$slot')
640                 FROM pg_replication_slots
641                 WHERE slot_name = '$slot';
642             " &>/dev/null || true
643         done
644     done
645
646     # Drop databases
647     for dbname in "${DATABASES_TO_CLEANUP[@]}"; do
648         drop_test_db "$dbname"
649     done
650
651     # Remove test directory
652     if [[ -d "$TEST_DIR" ]]; then
653         rm -rf "$TEST_DIR"
654     fi
655
656     log_info "Cleanup complete"
657 }
658
659 #
660 # Main test runner
661 #
662
663 main() {
664     echo "========================================"
665     echo "pg_scribe --new-chain Test Suite"
666     echo "========================================"
667     echo ""
668
669     # Verify pg_scribe exists
670     if [[ ! -x "$PG_SCRIBE" ]]; then
671         echo "ERROR: pg_scribe not found or not executable: $PG_SCRIBE"
672         exit 1
673     fi
674
675     # Verify PostgreSQL is running
676     if ! psql -U "$PGUSER" -d postgres -c "SELECT 1;" &>/dev/null; then
677         echo "ERROR: Cannot connect to PostgreSQL"
678         exit 1
679     fi
680
681     # Verify wal_level is logical
682     local wal_level
683     wal_level=$(psql -U "$PGUSER" -d postgres -tAq -c "SHOW wal_level;")
684     if [[ "$wal_level" != "logical" ]]; then
685         echo "ERROR: wal_level must be 'logical', currently: $wal_level"
686         echo "Update ~/.pgenv/pgsql/data/postgresql.conf and restart PostgreSQL"
687         exit 1
688     fi
689
690     # Create test directory
691     mkdir -p "$TEST_DIR"
692
693     # Set up cleanup trap
694     trap cleanup EXIT INT TERM
695
696     echo "Running tests..."
697     echo ""
698
699     # Run all tests (use || true to prevent set -e from exiting)
700     test_new_chain_requires_args || true
701     test_new_chain_directory_must_exist || true
702     test_new_chain_basic_success || true
703     test_new_chain_with_gzip_compression || true
704     test_new_chain_no_compression || true
705     test_new_chain_multiple_times || true
706     test_new_chain_restorability || true
707     test_new_chain_with_complex_schema || true
708     test_new_chain_metadata_tracking || true
709     test_new_chain_with_start_flag || true
710
711     # Summary
712     echo ""
713     echo "========================================"
714     echo "Test Results"
715     echo "========================================"
716     echo "Tests run:    $TESTS_RUN"
717     echo -e "Tests passed: ${GREEN}$TESTS_PASSED${NC}"
718     echo -e "Tests failed: ${RED}$TESTS_FAILED${NC}"
719     echo ""
720
721     if [[ $TESTS_FAILED -eq 0 ]]; then
722         echo -e "${GREEN}All tests passed!${NC}"
723         exit 0
724     else
725         echo -e "${RED}Some tests failed!${NC}"
726         exit 1
727     fi
728 }
729
730 main "$@"