#!/usr/bin/env perl # -*-mode:cperl; indent-tabs-mode: nil-*- ## Test using Postgres as a database target use 5.008003; use strict; use warnings; use Data::Dumper; use lib 't','.'; use DBD::Pg; use Test::More; use MIME::Base64; use File::Temp qw/ tempfile /; use Cwd; use vars qw/ $dbhX $dbhA $dbhB $dbhC $dbhD $dbhE $res $command $t $SQL %pkey %sth %sql $sth $count $val /; use BucardoTesting; my $bct = BucardoTesting->new({location => 'postgres'}) or BAIL_OUT "Creation of BucardoTesting object failed\n"; ## Some of the tests are called inside of BucardoTesting.pm ## e.g. $bct->check_for_row([[1]], [qw/ B C D/]); ## The above runs one test for each passed in database x the number of test tables my $numtables = keys %tabletype; my $numsequences = keys %sequences; my $single_tests = 63; my $check_for_row_1 = 1; my $check_for_row_2 = 2; my $check_for_row_3 = 3; my $check_for_row_4 = 7; my $check_sequences_same = 1; ## We have to set up the PGSERVICEFILE early on, so the proper ## environment variable is set for all processes from the beginning. my ($service_fh, $service_temp_filename) = tempfile("bucardo_pgservice.tmp.XXXX", UNLINK => 0); $ENV{PGSERVICEFILE} = getcwd . '/' . $service_temp_filename; plan tests => $single_tests + ( $check_sequences_same * $numsequences ) + ## Simple sequence testing ( $check_for_row_1 * $numtables * 1 ) + ## D ( $check_for_row_2 * $numtables * 2 ) + ## A B ( $check_for_row_3 * $numtables * 3 ) + ## B C D ( $check_for_row_4 * $numtables * 4 ); ## A B C D pass("*** Beginning postgres tests"); END { $bct and $bct->stop_bucardo(); $dbhX and $dbhX->disconnect(); $dbhA and $dbhA->disconnect(); $dbhB and $dbhB->disconnect(); $dbhC and $dbhC->disconnect(); $dbhD and $dbhD->disconnect(); $dbhE and $dbhE->disconnect(); } ## Get A, B, C, D, and E created, emptied out, and repopulated with sample data $dbhA = $bct->repopulate_cluster('A'); $dbhB = $bct->repopulate_cluster('B'); $dbhC = $bct->repopulate_cluster('C'); $dbhD = $bct->repopulate_cluster('D'); $dbhE = $bct->repopulate_cluster('E'); ## Create a bucardo database, and install Bucardo into it $dbhX = $bct->setup_bucardo('A'); ## Teach Bucardo about the first four databases for my $name (qw/ A B C D A1 /) { $t = "Adding database from cluster $name works"; my ($dbuser,$dbport,$dbhost) = $bct->add_db_args($name); $command = "bucardo add db $name dbname=bucardo_test user=$dbuser port=$dbport host=$dbhost status=active conn=sslmode=allow"; $res = $bct->ctl($command); like ($res, qr/Added database "$name"/, $t); } ## Teach Bucardo about the fifth database using a service file $t = "Adding database E via a service name works"; my ($dbuser,$dbport,$dbhost) = $bct->add_db_args('E'); print $service_fh "[dbE]\ndbname=bucardo_test\nuser=$dbuser\nport=$dbport\nhost=$dbhost\n"; close $service_fh; $res = $bct->ctl("add db E service=dbE status=inactive"); like ($res, qr/Added database "E"/, $t); ## Put all pk tables into a relgroup $t = q{Adding all PK tables on the master works}; $res = $bct->ctl(q{bucardo add tables '*bucardo*test*' '*Bucardo*test*' db=A relgroup=allpk pkonly}); like ($res, qr/Created the relgroup named "allpk".*are now part of/s, $t); ## Add all sequences $t = q{Adding all sequences to the main relgroup}; $res = $bct->ctl(q{bucardo add all sequences relgroup=allpk}); like ($res, qr/New sequences added/s, $t); ## Create a new dbgroup going from A to B and C and D and E $t = q{Created a new dbgroup A -> B C D E}; $res = $bct->ctl('bucardo add dbgroup pg1 A:source B:target C:target D:target E:target'); like ($res, qr/Created dbgroup "pg1"/, $t); ## Create a new dbgroup going from A and B to C and D $t = q{Created a new dbgroup (A <=> B ) -> C D}; $res = $bct->ctl('bucardo add dbgroup pg2 A:source B:source C D'); like ($res, qr/Created dbgroup "pg2"/, $t); ## Create a new dbgroup going from A and B and C to D $t = q{Created a new dbgroup (A <=> B <=> C) -> D}; $res = $bct->ctl('bucardo add dbgroup pg3 A:source B:source C:source D'); like ($res, qr/Created dbgroup "pg3"/, $t); ## Create a new dbgroup going from A and B and C and D $t = q{Created a new dbgroup (A <=> B <=> C <=> D)}; $res = $bct->ctl('bucardo add dbgroup pg4 A:source B:source C:source D:source'); like ($res, qr/Created dbgroup "pg4"/, $t); ## Create a new dbgroup going between A and B $t = q{Created a new dbgroup (A <=> B)}; $res = $bct->ctl('bucardo add dbgroup pg5 A:source B:source'); like ($res, qr/Created dbgroup "pg5"/, $t); ## Create some new syncs. Only one should be active at a time! $t = q{Created a new sync for dbgroup pg1}; $res = $bct->ctl('bucardo add sync pgtest1 relgroup=allpk dbs=pg1 status=inactive'); like ($res, qr/Added sync "pgtest1"/, $t); $t = q{Created a new sync for dbgroup pg2}; $res = $bct->ctl('bucardo add sync pgtest2 relgroup=allpk dbs=pg2 status=inactive autokick=false'); like ($res, qr/Added sync "pgtest2"/, $t); $t = q{Created a new sync for dbgroup pg3}; $res = $bct->ctl('bucardo add sync pgtest3 relgroup=allpk dbs=pg3 status=inactive autokick=false'); like ($res, qr/Added sync "pgtest3"/, $t); $t = q{Created a new sync for dbgroup pg4}; $res = $bct->ctl('bucardo add sync pgtest4 relgroup=allpk dbs=pg4 status=inactive autokick=false'); like ($res, qr/Added sync "pgtest4"/, $t); $t = q{Created a new sync for dbgroup pg5}; $res = $bct->ctl('bucardo add sync pgtest5 relgroup=allpk dbs=pg5 status=inactive autokick=false'); like ($res, qr/Added sync "pgtest5"/, $t); ## Create a table that only exists on A and B: make sure C does not look for it! $SQL = 'CREATE TABLE mtest(id INT PRIMARY KEY, email TEXT)'; $dbhA->do($SQL); $dbhA->commit(); $dbhB->do($SQL); $dbhB->commit(); ## Create a copy of table1, but with a different name for same-database replication testing $SQL = 'CREATE TABLE bucardo_test1_copy (LIKE bucardo_test1)'; $dbhA->do($SQL); $dbhA->commit(); $dbhB->do($SQL); $dbhB->commit(); ## Create a relgroup for same-database testing $t = q{Created a new relgroup samerelgroup}; $res = $bct->ctl('bucardo add relgroup samerelgroup bucardo_test1'); like ($res, qr/Created relgroup "samerelgroup"/, $t); ## We want all access to A1 to use the alternate table $t = q{Created a customname to force usage of bucardo_test1_copy}; $res = $bct->ctl('bucardo add customname bucardo_test1 bucardo_test1_copy db=A1'); like ($res, qr/\Qpublic.bucardo_test1 to bucardo_test1_copy (for database A1)/, $t); $t = q{Created a new sync for samedb}; $res = $bct->ctl('bucardo add sync samedb relgroup=samerelgroup dbs=A,A1 status=inactive'); like ($res, qr/Added sync "samedb"/, $t); ## Create new relgroups, relations, and a sync $t = q{Created a new relgroup mrelgroup}; $res = $bct->ctl('bucardo add relgroup mrelgroup mtest'); like ($res, qr/Created relgroup "mrelgroup"/, $t); $t = q{Created a new sync for mrelgroup}; $res = $bct->ctl('bucardo add sync msync relgroup=mrelgroup dbs=A:source,B:source status=inactive'); like ($res, qr/Added sync "msync"/, $t); ## Add a row to A, to make sure it does not go anywhere with inactive syncs $bct->add_row_to_database('A', 1); ## Clean out the droptest table for later testing $dbhA->do('TRUNCATE TABLE droptest_bucardo'); $dbhA->commit(); sub d { my $msg = shift || '?'; my $time = scalar localtime; diag "$time: $msg"; } ## Start up Bucardo. All syncs are inactive, so nothing should happen, ## and Bucardo should exit $bct->restart_bucardo($dbhX, 'bucardo_stopped'); # Nothing should have been copied to B, C, or D, yet. $bct->check_for_row([], [qw/B C D/]); ## Activate the pgtest1 and samedb syncs is $bct->ctl('bucardo update sync pgtest1 status=active'), '', 'Activate pgtest1'; is $bct->ctl('bucardo update sync samedb status=active'), '', 'Activate samedb'; ## Start listening for a syncdone message ## Bucardo should fire the sync off right away without a kick $dbhX->do('LISTEN bucardo_syncdone_pgtest1'); $dbhX->do('LISTEN bucardo_syncdone_samedb'); $dbhX->commit(); ## Create a lock file to test the forced file locking my $lockfile = 'pid/bucardo-force-lock-pgtest1'; open my $fh, '>', $lockfile or die qq{Could not create "$lockfile": $!\n}; close $fh; ## Start up Bucardo again $bct->restart_bucardo($dbhX); ## Wait for our sync to finish $bct->wait_for_notice($dbhX, 'bucardo_syncdone_pgtest1'); ## See if things are on the other databases $bct->check_for_row([[1]], [qw/ B C D/]); ## Check that our "samedb" process worked $t = q{Replicating to the same database via customname works}; $SQL = 'SELECT inty FROM bucardo_test1_copy'; $res = $dbhA->selectall_arrayref($SQL); is_deeply($res, [[1]], $t); ## Make sure triggers and rules did not fire $SQL = 'SELECT * FROM droptest_bucardo'; $sth = $dbhB->prepare($SQL); $count = $sth->execute(); if ($count >= 1) { diag Dumper $sth->fetchall_arrayref({}); BAIL_OUT "Found rows ($count) in the droptest table!"; } $sth->finish(); ok ('No rows found in the droptest table: triggers and rules were disabled'); ## Switch to a 2 source sync is $bct->ctl('bucardo update sync pgtest1 status=inactive'), '', 'Set pgtest1 status=inactive'; is $bct->ctl('bucardo update sync pgtest5 status=active'), '', 'Set pgtest5 status=active'; is $bct->ctl('bucardo deactivate pgtest1'), "Deactivating sync pgtest1\n", 'Deactivate pgtest1'; is $bct->ctl('bucardo activate pgtest5 0'), "Activating sync pgtest5...OK\n", 'Activate pgtest5'; ## Add some rows to both masters, make sure it goes everywhere $bct->add_row_to_database('A', 3); $bct->add_row_to_database('B', 4); ## Kick off the sync. my $timer_regex = qr/\[\d+\s*s\]\s+(?:[\b]{6}\[\d+\s*s\]\s+)*/; like $bct->ctl('bucardo kick sync pgtest5 0'), qr/^Kick\s+pgtest5:\s+${timer_regex}DONE!/, 'Kick pgtest5' or die 'Sync failed, no point continuing'; ## All rows should be on A and B. my $expected = [[1],[3],[4]]; $bct->check_for_row($expected, [qw/A B/]); # But new rows should not be on C or D. $bct->check_for_row([[1]], [qw/C D/]); ## Remove the test rows from above $bct->remove_row_from_database('A', [3,4]); $bct->remove_row_from_database('B', [3,4]); ## Switch to a 2 source, 2 target sync is $bct->ctl('bucardo update sync pgtest5 status=inactive'), '', 'set pgtest5 status=inactive'; is $bct->ctl('bucardo update sync pgtest2 status=active'), '', 'Set pgtest2 status=active'; is $bct->ctl('bucardo deactivate sync pgtest5'), "Deactivating sync pgtest5\n", 'Deactivate pgtest5'; is $bct->ctl('bucardo activate sync pgtest2 0'), "Activating sync pgtest2...OK\n", 'Activate pgtest2'; ## Clear the deleted rows above so we have a clean test below like $bct->ctl('bucardo kick sync pgtest2 0'), qr/^Kick\s+pgtest2:\s+${timer_regex}DONE!/, 'Kick pgtest2' or die 'Sync failed, no point continuing'; ## Add some rows to both masters, make sure it goes everywhere for my $num (2..4) { $bct->add_row_to_database('A', $num); } for my $num (5..10) { $bct->add_row_to_database('B', $num); } ## Kick off the sync. Everything should go to A, B, C, and D like $bct->ctl('bucardo kick sync pgtest2 0'), qr/^Kick\s+pgtest2:\s+${timer_regex}DONE!/, 'Kick pgtest2 again' or die 'Sync failed, no point continuing'; ## Kick off old sync. Should fail, as the sync is inactive $t = q{Inactive sync pgtest3 should not reject kick}; $res = $bct->ctl('bucardo kick sync pgtest3 0'); like($res, qr/^Cannot kick inactive sync/, $t); ## All rows should be on A, B, C, and D $expected = []; push @$expected, [$_] for 1..10; $bct->check_for_row($expected, [qw/A B C D/]); ## Deactivate pgtest2, bring up pgtest3 is $bct->ctl('bucardo update sync pgtest2 status=inactive'), '', 'Set pgtest2 status=inactive'; is $bct->ctl('bucardo update sync pgtest3 status=active'), '', 'Set pgtest3 status=active'; is $bct->ctl('bucardo deactivate sync pgtest2'), "Deactivating sync pgtest2\n", 'Deactivate pgtest2'; is $bct->ctl('bucardo activate sync pgtest3 0'), "Activating sync pgtest3...OK\n", 'Activate pgtest3'; ## Kick off the sync to pick up the deltas from the previous runs like $bct->ctl('bucardo kick sync pgtest3 0'), qr/^Kick\s+pgtest3:\s+${timer_regex}DONE!/, 'Kick pgtest3' or die 'Sync failed, no point continuing'; ## This one has three sources: A, B, and C. Remove rows from each $bct->remove_row_from_database('A', 10); $bct->remove_row_from_database('A', 9); $bct->remove_row_from_database('A', 8); $bct->remove_row_from_database('B', 6); $bct->remove_row_from_database('B', 5); $bct->remove_row_from_database('B', 4); $bct->remove_row_from_database('C', 2); $bct->remove_row_from_database('C', 1); ## Kick it off like $bct->ctl('bucardo kick sync pgtest3 0'), qr/^Kick\s+pgtest3:\s+${timer_regex}DONE!/, 'Kick pgtest3 again' or die 'Sync failed, no point continuing'; ## Only rows left everywhere should be 3 and 7 $bct->check_for_row([[3],[7]], [qw/A B C D/]); ## Cause a conflict: same row on A, B, and C. $bct->add_row_to_database('A', 1); $bct->add_row_to_database('B', 1); $bct->add_row_to_database('A', 2); $bct->add_row_to_database('B', 2); $bct->add_row_to_database('C', 2); ## Kick and check everyone is the same like $bct->ctl('bucardo kick sync pgtest3 0'), qr/^Kick\s+pgtest3:\s+${timer_regex}DONE!/, 'Kick pgtest3 yet again' or die 'Sync failed, no point continuing'; $bct->check_for_row([[1],[2],[3],[7]], [qw/A B C D/]); ## Change sequence information, make sure it gets out to everyone if ($dbhA->{pg_server_version} < 80400) { $dbhA->do('alter sequence bucardo_test_seq1 restart 25 minvalue 10 maxvalue 8675'); $dbhB->do('alter sequence bucardo_test_seq2 restart 250 minvalue 100 maxvalue 86753'); } else { $dbhA->do('alter sequence bucardo_test_seq1 start 20 restart 25 minvalue 10 maxvalue 8675'); $dbhB->do('alter sequence bucardo_test_seq2 start 200 restart 250 minvalue 100 maxvalue 86753'); } $dbhA->commit(); $dbhB->commit(); $dbhC->do(q{SELECT setval('"Bucardo_test_seq3"', 12345)}); $dbhC->commit(); like $bct->ctl('bucardo kick sync pgtest3 0'), qr/^Kick\s+pgtest3:\s+${timer_regex}DONE!/, 'Kick pgtest3 once more' or die 'Sync failed, no point continuing'; $bct->check_sequences_same([qw/A B C D/]); ## Create a PK conflict and let B "win" due to the timestamp $SQL = 'UPDATE bucardo_test1 SET data1 = ? WHERE id = ?'; $dbhB->do($SQL, {}, 'Bravo', 3); $dbhC->do($SQL, undef, 'Charlie', 3); $dbhA->do($SQL, undef, 'Alpha', 3); ## Order of commits should not matter: the timestamp comes from the start of the transaction $dbhC->commit(); $dbhB->commit(); $dbhA->commit(); ## Just in case, make sure 'bucardo upgrade' does not mess anything up $bct->ctl('bucardo upgrade'); like $bct->ctl('bucardo kick sync pgtest3 0'), qr/^Kick\s+pgtest3:\s+${timer_regex}DONE!/, 'Kick pgtest3 LIKE A BOSS' or die 'Sync failed, no point continuing'; $bct->check_for_row([[1],[2],[3],[7]], [qw/A B C D/]); $SQL = 'SELECT data1 FROM bucardo_test1 WHERE id = ?'; $val = $dbhA->selectall_arrayref($SQL, undef, 3)->[0][0]; $t = 'Conflict resolution respects earliest transaction time for A'; is ($val, 'Charlie', $t); $t = 'Conflict resolution respects earliest transaction time for B'; $val = $dbhB->selectall_arrayref($SQL, undef, 3)->[0][0]; is ($val, 'Charlie', $t); $t = 'Conflict resolution respects earliest transaction time for C'; $val = $dbhC->selectall_arrayref($SQL, undef, 3)->[0][0]; is ($val, 'Charlie', $t); if ($dbhA->{pg_server_version} < 80400) { ## Truncate triggers do not work, so we will delete instead $bct->delete_all_tables('A'); } else { ## Truncate on A:source goes to all other nodes $bct->truncate_all_tables('A'); ## Just for fun, let C win a truncation "contest" $dbhC->do('TRUNCATE TABLE bucardo_test5'); ## We commit everyone as the truncates will block on open transactions $dbhX->commit(); $dbhA->commit(); $dbhB->commit(); $dbhC->commit(); $dbhD->commit(); } like $bct->ctl('bucardo kick sync pgtest3 0'), qr/^Kick\s+pgtest3:\s+${timer_regex}DONE!/, 'Kick pgtest3 like a mofo' or die 'Sync failed, no point continuing'; $bct->check_for_row([], [qw/A B C D/], 'truncate A'); if ($dbhA->{pg_server_version} < 80400) { ## Truncate triggers do not work, so we will delete instead $bct->delete_all_tables('A'); ## We do this to emulate all the stuff below $bct->add_row_to_database('A', 7); $bct->add_row_to_database('A', 3); SKIP: { skip 'Skipping truncate tests', 5; } $dbhA->commit(); } else { ## A truncate plus delta rows will truncate all others but keep delta rows $bct->add_row_to_database('A', 1); $bct->add_row_to_database('B', 2); $bct->add_row_to_database('C', 3); $bct->add_row_to_database('D', 4); ## Order matters: the last one should "win" and thus replicate subsequent changes for my $d (qw/ A B C D /) { $bct->truncate_all_tables($d); } ## Now add some things back to each one $bct->add_row_to_database('A', 5); $bct->add_row_to_database('B', 6); $bct->add_row_to_database('C', 7); $bct->add_row_to_database('D', 8); ## Kick off the sync. C should win (D is target), truncate the others, then propagate '7' like $bct->ctl('bucardo kick sync pgtest3 0'), qr/^Kick\s+pgtest3:\s+${timer_regex}DONE!/, 'Kick pgtest3 like a it\'s going out of style' or die 'Sync failed, no point continuing'; $bct->check_for_row([[7]], [qw/A B C D/], 'truncate D'); } ## Make sure we can go back to normal mode after a truncate $bct->add_row_to_database('A', 2); $bct->add_row_to_database('B', 3); like $bct->ctl('bucardo kick sync pgtest3 0'), qr/^Kick\s+pgtest3:\s+${timer_regex}DONE!/, 'Kick pgtest3 like it hurts' or die 'Sync failed, no point continuing'; $bct->check_for_row([[2],[3],[7]], [qw/A B C D/]); ## Tests of customcols $t = q{add customcols returns expected message}; $res = $bct->ctl('bucardo add customcols bucardo_test1 "SELECT id, data1, inty*30 AS inty"'); like($res, qr/\QNew columns for public.bucardo_test1: "SELECT id, data1, inty*30 AS inty"/, $t); ## Also test the rebuild_index functionality $res = $bct->ctl('bucardo update sync pgtest3 rebuild_index=1'); ## We need to restart Bucardo entirely to change this. Someday, a reload sync will be enough. $bct->restart_bucardo($dbhX); $bct->add_row_to_database('A', 1); like $bct->ctl('bucardo kick sync pgtest3 0'), qr/^Kick\s+pgtest3:\s+${timer_regex}DONE!/, 'Kick pgtest3 again for the road' or die 'Sync failed, no point continuing'; $bct->check_for_row([[1],[2],[3],[7]], [qw/A B C/]); $bct->check_for_row([[1],[2],[3],[7]], [qw/D/], 'customcols', '!test1'); $bct->check_for_row([[2],[3],[7],[30]], [qw/D/], 'customcols', 'test1'); unlink $service_temp_filename; $bct->ctl('bucardo stop'); pass('Finished with testing'); exit;