On Mon, Nov 30, 2015 at 6:28 AM, Noah Misch <n...@leadboat.com> wrote:
>
> On Fri, Nov 27, 2015 at 07:53:10PM -0300, Alvaro Herrera wrote:
> > Michael Paquier wrote:
> > > The result of a couple of hours of hacking is attached:
> > > - 0001 is the refactoring adding PostgresNode and RecursiveCopy. I have
> > > also found that it is quite advantageous to move some of the routines that
> > > are synonyms of system() and the stuff used for logging into another
> > > low-level library that PostgresNode depends on, that I called TestBase in
> > > this patch.
>
> > Here's another version of this.  I changed the packages a bit more.  For
> > starters, I moved the routines around a bit; some of your choices seemed
> > more about keeping stuff where it was originally rather than moving it
> > to where it made sense.  These are the routines in each module:
> >
> > TestBase:  system_or_bail system_log run_log slurp_dir slurp_file
> > append_to_file
> >
> > TestLib:    get_new_node teardown_node psql poll_query_until command_ok
> > command_fails command_exit_is program_help_ok program_version_ok
> > program_options_handling_ok command_like issues_sql_like
>
> The proposed code is short on guidance about when to put a function in TestLib
> versus TestBase.  TestLib has no header comment.  The TestBase header comment
> would permit, for example, command_ok() in that module.  I would try instead
> keeping TestLib as the base module and moving into PostgresNode the functions
> that deal with PostgreSQL clusters (get_new_node teardown_node psql
> poll_query_until issues_sql_like).

PostgresNode is wanted to be a base representation of how of node is,
not of how to operate on it. The ways to perform the tests, which
works on a node, is wanted as a higher-level operation.

Logging and base configuration of a test set is a lower level of
operations than PostgresNode, because cluster nodes need actually to
perform system calls, some of those system calls like run_log allowing
to log in the centralized log file. I have tried to make the headers
of those modules more verbose, please see attached.

>
> > +my $node = get_new_node();
> > +# Initialize node without replication settings
> > +$node->initNode(0);
> > +$node->startNode();
> > +my $pgdata = $node->getDataDir();
> > +
> > +$ENV{PGPORT} = $node->getPort();
>
> Starting a value retrieval method name with "get" is not Perlish.  The TAP
> suites currently follow "man perlstyle" in using underscored_lower_case method
> names.  No PostgreSQL Perl code uses lowerFirstCamelCase, though some uses
> CamelCase.  The word "Node" is redundant.  Use this style:
>
>   $node->init(0);
>   $node->start;
>   my $pgdata = $node->data_dir;
>   $ENV{PGPORT} = $node->port;

I have switched the style this way.

> As a matter of opinion, I recommend giving "init" key/value arguments instead
> of the single Boolean argument.  The method could easily need more options in
> the future, and this makes the call site self-documenting:
>
>   $node->init(hba_permit_replication => 0);

Done.

>
> > -     'pg_controldata with nonexistent directory fails');
> > +                       'pg_controldata with nonexistent directory fails');
>
> perltidy will undo this whitespace-only change.

Cleaned up.

>
> > --- a/src/bin/pg_rewind/t/001_basic.pl
> > +++ b/src/bin/pg_rewind/t/001_basic.pl
> > @@ -1,9 +1,11 @@
> > +# Basic pg_rewind test.
> > +
> >  use strict;
> >  use warnings;
> > -use TestLib;
> > -use Test::More tests => 8;
> >
> >  use RewindTest;
> > +use TestLib;
> > +use Test::More tests => 8;
>
> Revert all changes to this file.  Audit the rest of the patch for whitespace
> change unrelated to the subject.

Done.

>
>
> > -     'fails with nonexistent table');
> > +                       'fails with nonexistent table');
>
> > -'CREATE TABLE test1 (a int); CREATE INDEX test1x ON test1 (a); CLUSTER 
> > test1 USING test1x';
> > +     'CREATE TABLE test1 (a int); CREATE INDEX test1x ON test1 (a); 
> > CLUSTER test1 USING test1x';
>
> perltidy will undo these whitespace-only changes.

Cleaned up.

>
> > +# cluster -a is not compatible with -d, hence enforce environment variables
>
> s/cluster -a/clusterdb -a/

Fixed.

>
> > -command_fails([ 'createuser', 'user1' ], 'fails if role already exists');
> > +command_fails([ 'createuser', 'user1' ],
> > +                       'fails if role already exists');
>
> perltidy will undo this whitespace-only change.
>
> > @@ -0,0 +1,252 @@
> > +# PostgresNode, simple node representation for regression tests.
> > +#
> > +# Regression tests should use this basic class infrastructure to define 
> > nodes
> > +# that need used in the test modules/scripts.
> > +package PostgresNode;
>
> Consider just saying, "Class representing a data directory and postmaster."

OK, I have changed this description:
+# PostgresNode, class representing a data directory and postmaster.
+#
+# This contains a basic set of routines able to work on a PostgreSQL node,
+# allowing to start, stop, backup and initialize it with various options.

>
> > +     my $self   = {
> > +             _port     => undef,
> > +             _host     => undef,
> > +             _basedir  => undef,
> > +             _applname => undef,
> > +             _logfile  => undef };
> > +
> > +     # Set up each field
> > +     $self->{_port}     = $pgport;
> > +     $self->{_host}     = $pghost;
> > +     $self->{_basedir}  = TestBase::tempdir;
> > +     $self->{_applname} = "node_$pgport";
> > +     $self->{_logfile}  = "$TestBase::log_path/node_$pgport.log";
>
> Why set fields to undef immediately before filling them?

Fixed.

>
> > @@ -0,0 +1,143 @@
> > +# Set of low-level routines dedicated to base tasks for regression tests, 
> > like
> > +# command execution and logging.
> > +#
> > +# This module should not depend on any other PostgreSQL regression test
> > +# modules.
> > +package TestBase;
>
> This is no mere set of routines.  Just "use"-ing this module creates some
> directories and alters stdin/stdout/stderr.

I have updated the description of this file.

>
> > +BEGIN
> > +{
> > +     $windows_os = $Config{osname} eq 'MSWin32' || $Config{osname} eq 
> > 'msys';
> > +
> > +     # Determine output directories, and create them.  The base path is the
> > +     # TESTDIR environment variable, which is normally set by the invoking
> > +     # Makefile.
> > +     $tmp_check = $ENV{TESTDIR} ? "$ENV{TESTDIR}/tmp_check" : "tmp_check";
> > +     $log_path = "$tmp_check/log";
> > +
> > +     mkdir $tmp_check;
> > +     mkdir $log_path;
>
> Never mutate the filesystem in a BEGIN block, because "perl -c" runs BEGIN
> blocks.  (Likewise for the BEGIN block this patch adds to TestLib.)

Hm. It seems to me that the whole block should be part of INIT then,
because the log file where STDERR and STDOUT is recaptured depends on
those to be created as well. By doing this change, please note that
compilation errors are not recaptured into the log file (thanks Andrew
for the pointers to perlmod).

I have as well updated pg_rewind tests to remove PGDATABASE. Patch to
address those issues is attached.
Regards,
-- 
Michael
diff --git a/src/bin/initdb/t/001_initdb.pl b/src/bin/initdb/t/001_initdb.pl
index 299dcf5..3b5d7af 100644
--- a/src/bin/initdb/t/001_initdb.pl
+++ b/src/bin/initdb/t/001_initdb.pl
@@ -4,10 +4,11 @@
 
 use strict;
 use warnings;
+use TestBase;
 use TestLib;
 use Test::More tests => 14;
 
-my $tempdir = TestLib::tempdir;
+my $tempdir = TestBase::tempdir;
 my $xlogdir = "$tempdir/pgxlog";
 my $datadir = "$tempdir/data";
 
diff --git a/src/bin/pg_basebackup/t/010_pg_basebackup.pl b/src/bin/pg_basebackup/t/010_pg_basebackup.pl
index dc96bbf..c4e1d16 100644
--- a/src/bin/pg_basebackup/t/010_pg_basebackup.pl
+++ b/src/bin/pg_basebackup/t/010_pg_basebackup.pl
@@ -2,6 +2,8 @@ use strict;
 use warnings;
 use Cwd;
 use Config;
+use PostgresNode;
+use TestBase;
 use TestLib;
 use Test::More tests => 51;
 
@@ -9,8 +11,15 @@ program_help_ok('pg_basebackup');
 program_version_ok('pg_basebackup');
 program_options_handling_ok('pg_basebackup');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $tempdir = TestBase::tempdir;
+
+my $node = get_new_node();
+# Initialize node without replication settings
+$node->init(hba_permit_replication => 0);
+$node->start;
+my $pgdata = $node->data_dir;
+
+$ENV{PGPORT} = $node->port;
 
 command_fails(['pg_basebackup'],
 	'pg_basebackup needs target directory specified');
@@ -26,19 +35,19 @@ if (open BADCHARS, ">>$tempdir/pgdata/FOO\xe0\xe0\xe0BAR")
 	close BADCHARS;
 }
 
-configure_hba_for_replication "$tempdir/pgdata";
-system_or_bail 'pg_ctl', '-D', "$tempdir/pgdata", 'reload';
+$node->set_replication_conf();
+system_or_bail 'pg_ctl', '-D', $pgdata, 'reload';
 
 command_fails(
 	[ 'pg_basebackup', '-D', "$tempdir/backup" ],
 	'pg_basebackup fails because of WAL configuration');
 
-open CONF, ">>$tempdir/pgdata/postgresql.conf";
+open CONF, ">>$pgdata/postgresql.conf";
 print CONF "max_replication_slots = 10\n";
 print CONF "max_wal_senders = 10\n";
 print CONF "wal_level = archive\n";
 close CONF;
-restart_test_server;
+$node->restart;
 
 command_ok([ 'pg_basebackup', '-D', "$tempdir/backup" ],
 	'pg_basebackup runs');
@@ -81,13 +90,13 @@ command_fails(
 
 # Tar format doesn't support filenames longer than 100 bytes.
 my $superlongname = "superlongname_" . ("x" x 100);
-my $superlongpath = "$tempdir/pgdata/$superlongname";
+my $superlongpath = "$pgdata/$superlongname";
 
 open FILE, ">$superlongpath" or die "unable to create file $superlongpath";
 close FILE;
 command_fails([ 'pg_basebackup', '-D', "$tempdir/tarbackup_l1", '-Ft' ],
 	'pg_basebackup tar with long name fails');
-unlink "$tempdir/pgdata/$superlongname";
+unlink "$pgdata/$superlongname";
 
 # The following tests test symlinks. Windows doesn't have symlinks, so
 # skip on Windows.
@@ -98,7 +107,7 @@ SKIP: {
 	# to our physical temp location.  That way we can use shorter names
 	# for the tablespace directories, which hopefully won't run afoul of
 	# the 99 character length limit.
-	my $shorter_tempdir = tempdir_short . "/tempdir";
+	my $shorter_tempdir = TestBase::tempdir_short . "/tempdir";
 	symlink "$tempdir", $shorter_tempdir;
 
 	mkdir "$tempdir/tblspc1";
@@ -120,7 +129,7 @@ SKIP: {
 			"-T$shorter_tempdir/tblspc1=$tempdir/tbackup/tblspc1" ],
 		'plain format with tablespaces succeeds with tablespace mapping');
 	ok(-d "$tempdir/tbackup/tblspc1", 'tablespace was relocated');
-	opendir(my $dh, "$tempdir/pgdata/pg_tblspc") or die;
+	opendir(my $dh, "$pgdata/pg_tblspc") or die;
 	ok( (   grep {
 		-l "$tempdir/backup1/pg_tblspc/$_"
 			and readlink "$tempdir/backup1/pg_tblspc/$_" eq
diff --git a/src/bin/pg_controldata/t/001_pg_controldata.pl b/src/bin/pg_controldata/t/001_pg_controldata.pl
index e2b0d42..ae45f41 100644
--- a/src/bin/pg_controldata/t/001_pg_controldata.pl
+++ b/src/bin/pg_controldata/t/001_pg_controldata.pl
@@ -1,16 +1,19 @@
 use strict;
 use warnings;
+use PostgresNode;
 use TestLib;
 use Test::More tests => 13;
 
-my $tempdir = TestLib::tempdir;
-
 program_help_ok('pg_controldata');
 program_version_ok('pg_controldata');
 program_options_handling_ok('pg_controldata');
 command_fails(['pg_controldata'], 'pg_controldata without arguments fails');
 command_fails([ 'pg_controldata', 'nonexistent' ],
 	'pg_controldata with nonexistent directory fails');
-standard_initdb "$tempdir/data";
-command_like([ 'pg_controldata', "$tempdir/data" ],
+
+my $node = get_new_node();
+$node->init;
+$node->start;
+
+command_like([ 'pg_controldata', $node->data_dir ],
 	qr/checkpoint/, 'pg_controldata produces output');
diff --git a/src/bin/pg_ctl/t/001_start_stop.pl b/src/bin/pg_ctl/t/001_start_stop.pl
index f57abce..d76fe80 100644
--- a/src/bin/pg_ctl/t/001_start_stop.pl
+++ b/src/bin/pg_ctl/t/001_start_stop.pl
@@ -1,11 +1,12 @@
 use strict;
 use warnings;
 use Config;
+use TestBase;
 use TestLib;
 use Test::More tests => 17;
 
-my $tempdir       = TestLib::tempdir;
-my $tempdir_short = TestLib::tempdir_short;
+my $tempdir       = TestBase::tempdir;
+my $tempdir_short = TestBase::tempdir_short;
 
 program_help_ok('pg_ctl');
 program_version_ok('pg_ctl');
diff --git a/src/bin/pg_ctl/t/002_status.pl b/src/bin/pg_ctl/t/002_status.pl
index 31f7c72..2a9e0a5 100644
--- a/src/bin/pg_ctl/t/002_status.pl
+++ b/src/bin/pg_ctl/t/002_status.pl
@@ -1,22 +1,25 @@
 use strict;
 use warnings;
+use PostgresNode;
+use TestBase;
 use TestLib;
 use Test::More tests => 3;
 
-my $tempdir       = TestLib::tempdir;
-my $tempdir_short = TestLib::tempdir_short;
+my $tempdir       = TestBase::tempdir;
+my $tempdir_short = TestBase::tempdir_short;
 
 command_exit_is([ 'pg_ctl', 'status', '-D', "$tempdir/nonexistent" ],
 	4, 'pg_ctl status with nonexistent directory');
 
-standard_initdb "$tempdir/data";
+my $node = get_new_node();
+$node->init;
 
-command_exit_is([ 'pg_ctl', 'status', '-D', "$tempdir/data" ],
+command_exit_is([ 'pg_ctl', 'status', '-D', $node->data_dir ],
 	3, 'pg_ctl status with server not running');
 
 system_or_bail 'pg_ctl', '-l', "$tempdir/logfile", '-D',
-  "$tempdir/data", '-w', 'start';
-command_exit_is([ 'pg_ctl', 'status', '-D', "$tempdir/data" ],
+  $node->data_dir, '-w', 'start';
+command_exit_is([ 'pg_ctl', 'status', '-D', $node->data_dir ],
 	0, 'pg_ctl status with server running');
 
-system_or_bail 'pg_ctl', 'stop', '-D', "$tempdir/data", '-m', 'fast';
+system_or_bail 'pg_ctl', 'stop', '-D', $node->data_dir, '-m', 'fast';
diff --git a/src/bin/pg_rewind/RewindTest.pm b/src/bin/pg_rewind/RewindTest.pm
index a4c1737..24fd19c 100644
--- a/src/bin/pg_rewind/RewindTest.pm
+++ b/src/bin/pg_rewind/RewindTest.pm
@@ -9,22 +9,20 @@ package RewindTest;
 # To run a test, the test script (in t/ subdirectory) calls the functions
 # in this module. These functions should be called in this sequence:
 #
-# 1. init_rewind_test - sets up log file etc.
+# 1. setup_cluster - creates a PostgreSQL cluster that runs as the master
 #
-# 2. setup_cluster - creates a PostgreSQL cluster that runs as the master
+# 2. start_master - starts the master server
 #
-# 3. start_master - starts the master server
-#
-# 4. create_standby - runs pg_basebackup to initialize a standby server, and
+# 3. create_standby - runs pg_basebackup to initialize a standby server, and
 #    sets it up to follow the master.
 #
-# 5. promote_standby - runs "pg_ctl promote" to promote the standby server.
+# 4. promote_standby - runs "pg_ctl promote" to promote the standby server.
 # The old master keeps running.
 #
-# 6. run_pg_rewind - stops the old master (if it's still running) and runs
+# 5. run_pg_rewind - stops the old master (if it's still running) and runs
 # pg_rewind to synchronize it with the now-promoted standby server.
 #
-# 7. clean_rewind_test - stops both servers used in the test, if they're
+# 6. clean_rewind_test - stops both servers used in the test, if they're
 # still running.
 #
 # The test script can use the helper functions master_psql and standby_psql
@@ -37,27 +35,23 @@ package RewindTest;
 use strict;
 use warnings;
 
-use TestLib;
-use Test::More;
-
 use Config;
+use Exporter 'import';
 use File::Copy;
 use File::Path qw(rmtree);
 use IPC::Run qw(run start);
+use TestBase;
+use TestLib;
+use Test::More;
 
-use Exporter 'import';
 our @EXPORT = qw(
-  $connstr_master
-  $connstr_standby
-  $test_master_datadir
-  $test_standby_datadir
+  $node_master
+  $node_standby
 
-  append_to_file
   master_psql
   standby_psql
   check_query
 
-  init_rewind_test
   setup_cluster
   start_master
   create_standby
@@ -66,32 +60,24 @@ our @EXPORT = qw(
   clean_rewind_test
 );
 
-our $test_master_datadir  = "$tmp_check/data_master";
-our $test_standby_datadir = "$tmp_check/data_standby";
-
-# Define non-conflicting ports for both nodes.
-my $port_master  = $ENV{PGPORT};
-my $port_standby = $port_master + 1;
-
-my $connstr_master  = "port=$port_master";
-my $connstr_standby = "port=$port_standby";
-
-$ENV{PGDATABASE} = "postgres";
+# Our nodes.
+our $node_master;
+our $node_standby;
 
 sub master_psql
 {
 	my $cmd = shift;
 
-	system_or_bail 'psql', '-q', '--no-psqlrc', '-d', $connstr_master,
-	  '-c', "$cmd";
+	system_or_bail 'psql', '-q', '--no-psqlrc', '-d',
+	  $node_master->connstr('postgres'), '-c', "$cmd";
 }
 
 sub standby_psql
 {
 	my $cmd = shift;
 
-	system_or_bail 'psql', '-q', '--no-psqlrc', '-d', $connstr_standby,
-	  '-c', "$cmd";
+	system_or_bail 'psql', '-q', '--no-psqlrc', '-d',
+      $node_standby->connstr('postgres'), '-c', "$cmd";
 }
 
 # Run a query against the master, and check that the output matches what's
@@ -104,7 +90,7 @@ sub check_query
 	# we want just the output, no formatting
 	my $result = run [
 		'psql',          '-q', '-A', '-t', '--no-psqlrc', '-d',
-		$connstr_master, '-c', $query ],
+		$node_master->connstr('postgres'), '-c', $query ],
 	  '>', \$stdout, '2>', \$stderr;
 
 	# We don't use ok() for the exit code and stderr, because we want this
@@ -125,56 +111,14 @@ sub check_query
 	}
 }
 
-# Run a query once a second, until it returns 't' (i.e. SQL boolean true).
-sub poll_query_until
-{
-	my ($query, $connstr) = @_;
-
-	my $max_attempts = 30;
-	my $attempts     = 0;
-	my ($stdout, $stderr);
-
-	while ($attempts < $max_attempts)
-	{
-		my $cmd = [ 'psql', '-At', '-c', "$query", '-d', "$connstr" ];
-		my $result = run $cmd, '>', \$stdout, '2>', \$stderr;
-
-		chomp($stdout);
-		$stdout =~ s/\r//g if $Config{osname} eq 'msys';
-		if ($stdout eq "t")
-		{
-			return 1;
-		}
-
-		# Wait a second before retrying.
-		sleep 1;
-		$attempts++;
-	}
-
-	# The query result didn't change in 30 seconds. Give up. Print the stderr
-	# from the last attempt, hopefully that's useful for debugging.
-	diag $stderr;
-	return 0;
-}
-
-sub append_to_file
-{
-	my ($filename, $str) = @_;
-
-	open my $fh, ">>", $filename or die "could not open file $filename";
-	print $fh $str;
-	close $fh;
-}
-
 sub setup_cluster
 {
 	# Initialize master, data checksums are mandatory
-	rmtree($test_master_datadir);
-	standard_initdb($test_master_datadir);
+	$node_master = get_new_node();
+	$node_master->init;
 
 	# Custom parameters for master's postgresql.conf
-	append_to_file(
-		"$test_master_datadir/postgresql.conf", qq(
+	$node_master->append_conf("postgresql.conf", qq(
 wal_level = hot_standby
 max_wal_senders = 2
 wal_keep_segments = 20
@@ -185,17 +129,11 @@ hot_standby = on
 autovacuum = off
 max_connections = 10
 ));
-
-	# Accept replication connections on master
-	configure_hba_for_replication $test_master_datadir;
 }
 
 sub start_master
 {
-	system_or_bail('pg_ctl' , '-w',
-				   '-D' , $test_master_datadir,
-				   '-l',  "$log_path/master.log",
-				   "-o", "-p $port_master", 'start');
+	$node_master->start;
 
 	#### Now run the test-specific parts to initialize the master before setting
 	# up standby
@@ -203,24 +141,19 @@ sub start_master
 
 sub create_standby
 {
+	$node_standby = get_new_node();
+	$node_master->backup('my_backup');
+	$node_standby->init_from_backup($node_master, 'my_backup');
+	my $connstr_master = $node_master->connstr('postgres');
 
-	# Set up standby with necessary parameter
-	rmtree $test_standby_datadir;
-
-	# Base backup is taken with xlog files included
-	system_or_bail('pg_basebackup', '-D', $test_standby_datadir,
-				   '-p', $port_master, '-x');
-	append_to_file(
-		"$test_standby_datadir/recovery.conf", qq(
+	$node_standby->append_conf("recovery.conf", qq(
 primary_conninfo='$connstr_master application_name=rewind_standby'
 standby_mode=on
 recovery_target_timeline='latest'
 ));
 
 	# Start standby
-	system_or_bail('pg_ctl', '-w', '-D', $test_standby_datadir,
-				   '-l', "$log_path/standby.log",
-				   '-o', "-p $port_standby", 'start');
+	$node_standby->start;
 
 	# The standby may have WAL to apply before it matches the primary.  That
 	# is fine, because no test examines the standby before promotion.
@@ -234,14 +167,14 @@ sub promote_standby
 	# Wait for the standby to receive and write all WAL.
 	my $wal_received_query =
 "SELECT pg_current_xlog_location() = write_location FROM pg_stat_replication WHERE application_name = 'rewind_standby';";
-	poll_query_until($wal_received_query, $connstr_master)
+	poll_query_until($node_master, $wal_received_query)
 	  or die "Timed out while waiting for standby to receive and write WAL";
 
 	# Now promote slave and insert some new data on master, this will put
 	# the master out-of-sync with the standby. Wait until the standby is
 	# out of recovery mode, and is ready to accept read-write connections.
-	system_or_bail('pg_ctl', '-w', '-D', $test_standby_datadir, 'promote');
-	poll_query_until("SELECT NOT pg_is_in_recovery()", $connstr_standby)
+	system_or_bail('pg_ctl', '-w', '-D', $node_standby->data_dir, 'promote');
+	poll_query_until($node_standby, "SELECT NOT pg_is_in_recovery()")
 	  or die "Timed out while waiting for promotion of standby";
 
 	# Force a checkpoint after the promotion. pg_rewind looks at the control
@@ -256,9 +189,13 @@ sub promote_standby
 sub run_pg_rewind
 {
 	my $test_mode = shift;
+	my $master_pgdata = $node_master->data_dir;
+	my $standby_pgdata = $node_standby->data_dir;
+	my $standby_connstr = $node_standby->connstr('postgres');
+	my $tmp_folder = TestBase::tempdir;
 
 	# Stop the master and be ready to perform the rewind
-	system_or_bail('pg_ctl', '-D', $test_master_datadir, '-m', 'fast', 'stop');
+	$node_master->stop;
 
 	# At this point, the rewind processing is ready to run.
 	# We now have a very simple scenario with a few diverged WAL record.
@@ -267,20 +204,19 @@ sub run_pg_rewind
 
 	# Keep a temporary postgresql.conf for master node or it would be
 	# overwritten during the rewind.
-	copy("$test_master_datadir/postgresql.conf",
-		 "$tmp_check/master-postgresql.conf.tmp");
+	copy("$master_pgdata/postgresql.conf",
+		 "$tmp_folder/master-postgresql.conf.tmp");
 
 	# Now run pg_rewind
 	if ($test_mode eq "local")
 	{
 		# Do rewind using a local pgdata as source
 		# Stop the master and be ready to perform the rewind
-		system_or_bail('pg_ctl', '-D', $test_standby_datadir,
-					   '-m', 'fast', 'stop');
+		$node_standby->stop;
 		command_ok(['pg_rewind',
 					"--debug",
-					"--source-pgdata=$test_standby_datadir",
-					"--target-pgdata=$test_master_datadir"],
+					"--source-pgdata=$standby_pgdata",
+					"--target-pgdata=$master_pgdata"],
 				   'pg_rewind local');
 	}
 	elsif ($test_mode eq "remote")
@@ -289,33 +225,30 @@ sub run_pg_rewind
 		command_ok(['pg_rewind',
 					"--debug",
 					"--source-server",
-					"port=$port_standby dbname=postgres",
-					"--target-pgdata=$test_master_datadir"],
+					$standby_connstr,
+					"--target-pgdata=$master_pgdata"],
 				   'pg_rewind remote');
 	}
 	else
 	{
-
 		# Cannot come here normally
 		die("Incorrect test mode specified");
 	}
 
 	# Now move back postgresql.conf with old settings
-	move("$tmp_check/master-postgresql.conf.tmp",
-		 "$test_master_datadir/postgresql.conf");
+	move("$tmp_folder/master-postgresql.conf.tmp",
+		 "$master_pgdata/postgresql.conf");
 
 	# Plug-in rewound node to the now-promoted standby node
-	append_to_file(
-		"$test_master_datadir/recovery.conf", qq(
+	my $port_standby = $node_standby->port;
+	$node_master->append_conf('recovery.conf', qq(
 primary_conninfo='port=$port_standby'
 standby_mode=on
 recovery_target_timeline='latest'
 ));
 
 	# Restart the master to check that rewind went correctly
-	system_or_bail('pg_ctl', '-w', '-D', $test_master_datadir,
-				   '-l', "$log_path/master.log",
-				   '-o', "-p $port_master", 'start');
+	$node_master->restart;
 
 	#### Now run the test-specific parts to check the result
 }
@@ -323,22 +256,8 @@ recovery_target_timeline='latest'
 # Clean up after the test. Stop both servers, if they're still running.
 sub clean_rewind_test
 {
-	if ($test_master_datadir)
-	{
-		system
-		  'pg_ctl', '-D', $test_master_datadir, '-m', 'immediate', 'stop';
-	}
-	if ($test_standby_datadir)
-	{
-		system
-		  'pg_ctl', '-D', $test_standby_datadir, '-m', 'immediate', 'stop';
-	}
+	teardown_node($node_master) if (defined($node_master));
+	teardown_node($node_standby) if (defined($node_standby));
 }
 
-# Stop the test servers, just in case they're still running.
-END
-{
-	my $save_rc = $?;
-	clean_rewind_test();
-	$? = $save_rc;
-}
+1;
diff --git a/src/bin/pg_rewind/t/003_extrafiles.pl b/src/bin/pg_rewind/t/003_extrafiles.pl
index d317f53..d196367 100644
--- a/src/bin/pg_rewind/t/003_extrafiles.pl
+++ b/src/bin/pg_rewind/t/003_extrafiles.pl
@@ -2,14 +2,13 @@
 
 use strict;
 use warnings;
+use TestBase;
 use TestLib;
 use Test::More tests => 4;
 
 use File::Find;
-
 use RewindTest;
 
-
 sub run_test
 {
 	my $test_mode = shift;
@@ -17,7 +16,7 @@ sub run_test
 	RewindTest::setup_cluster();
 	RewindTest::start_master();
 
-	my $test_master_datadir = $RewindTest::test_master_datadir;
+	my $test_master_datadir = $node_master->data_dir;
 
 	# Create a subdir and files that will be present in both
 	mkdir "$test_master_datadir/tst_both_dir";
@@ -30,6 +29,7 @@ sub run_test
 	RewindTest::create_standby();
 
 	# Create different subdirs and files in master and standby
+	my $test_standby_datadir = $node_standby->data_dir;
 
 	mkdir "$test_standby_datadir/tst_standby_dir";
 	append_to_file "$test_standby_datadir/tst_standby_dir/standby_file1",
diff --git a/src/bin/pg_rewind/t/004_pg_xlog_symlink.pl b/src/bin/pg_rewind/t/004_pg_xlog_symlink.pl
index c5f72e2..e05f55e 100644
--- a/src/bin/pg_rewind/t/004_pg_xlog_symlink.pl
+++ b/src/bin/pg_rewind/t/004_pg_xlog_symlink.pl
@@ -5,7 +5,7 @@ use strict;
 use warnings;
 use File::Copy;
 use File::Path qw(rmtree);
-use TestLib;
+use TestBase;
 use Test::More;
 if ($windows_os)
 {
@@ -23,11 +23,13 @@ sub run_test
 {
 	my $test_mode = shift;
 
-	my $master_xlogdir = "$tmp_check/xlog_master";
+	my $master_xlogdir = "${TestBase::tmp_check}/xlog_master";
 
 	rmtree($master_xlogdir);
 	RewindTest::setup_cluster();
 
+	my $test_master_datadir = $node_master->data_dir;
+
 	# turn pg_xlog into a symlink
 	print("moving $test_master_datadir/pg_xlog to $master_xlogdir\n");
 	move("$test_master_datadir/pg_xlog", $master_xlogdir) or die;
diff --git a/src/bin/scripts/t/010_clusterdb.pl b/src/bin/scripts/t/010_clusterdb.pl
index dc0d78a..1db7fdb 100644
--- a/src/bin/scripts/t/010_clusterdb.pl
+++ b/src/bin/scripts/t/010_clusterdb.pl
@@ -7,20 +7,26 @@ program_help_ok('clusterdb');
 program_version_ok('clusterdb');
 program_options_handling_ok('clusterdb');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
+
+$ENV{PGPORT} = $node->port;
+$ENV{PGDATABASE} = 'postgres';
 
 issues_sql_like(
-	[ 'clusterdb', 'postgres' ],
+	$node,
+	[ 'clusterdb' ],
 	qr/statement: CLUSTER;/,
 	'SQL CLUSTER run');
 
-command_fails([ 'clusterdb', '-t', 'nonexistent', 'postgres' ],
-	'fails with nonexistent table');
+command_fails([ 'clusterdb', '-t', 'nonexistent' ],
+			  'fails with nonexistent table');
 
 psql 'postgres',
-'CREATE TABLE test1 (a int); CREATE INDEX test1x ON test1 (a); CLUSTER test1 USING test1x';
+	'CREATE TABLE test1 (a int); CREATE INDEX test1x ON test1 (a); CLUSTER test1 USING test1x';
 issues_sql_like(
-	[ 'clusterdb', '-t', 'test1', 'postgres' ],
+	$node,
+	[ 'clusterdb', '-t', 'test1' ],
 	qr/statement: CLUSTER test1;/,
 	'cluster specific table');
diff --git a/src/bin/scripts/t/011_clusterdb_all.pl b/src/bin/scripts/t/011_clusterdb_all.pl
index 7769f70..22095ae 100644
--- a/src/bin/scripts/t/011_clusterdb_all.pl
+++ b/src/bin/scripts/t/011_clusterdb_all.pl
@@ -3,10 +3,17 @@ use warnings;
 use TestLib;
 use Test::More tests => 2;
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
+
+# clusterdb -a is not compatible with -d, hence enforce environment variables
+# correctly.
+$ENV{PGDATABASE} = 'postgres';
+$ENV{PGPORT} = $node->port;
 
 issues_sql_like(
+	$node,
 	[ 'clusterdb', '-a' ],
 	qr/statement: CLUSTER.*statement: CLUSTER/s,
 	'cluster all databases');
diff --git a/src/bin/scripts/t/020_createdb.pl b/src/bin/scripts/t/020_createdb.pl
index a44283c..01088ed 100644
--- a/src/bin/scripts/t/020_createdb.pl
+++ b/src/bin/scripts/t/020_createdb.pl
@@ -7,14 +7,19 @@ program_help_ok('createdb');
 program_version_ok('createdb');
 program_options_handling_ok('createdb');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
+
+$ENV{PGPORT} = $node->port;
 
 issues_sql_like(
+	$node,
 	[ 'createdb', 'foobar1' ],
 	qr/statement: CREATE DATABASE foobar1/,
 	'SQL CREATE DATABASE run');
 issues_sql_like(
+	$node,
 	[ 'createdb', '-l', 'C', '-E', 'LATIN1', '-T', 'template0', 'foobar2' ],
 	qr/statement: CREATE DATABASE foobar2 ENCODING 'LATIN1'/,
 	'create database with encoding');
diff --git a/src/bin/scripts/t/030_createlang.pl b/src/bin/scripts/t/030_createlang.pl
index 7ff0a3e..9cbe781 100644
--- a/src/bin/scripts/t/030_createlang.pl
+++ b/src/bin/scripts/t/030_createlang.pl
@@ -7,18 +7,23 @@ program_help_ok('createlang');
 program_version_ok('createlang');
 program_options_handling_ok('createlang');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
+
+$ENV{PGPORT} = $node->port;
+$ENV{PGDATABASE} = 'postgres';
 
 command_fails(
-	[ 'createlang', 'plpgsql', 'postgres' ],
+	[ 'createlang', 'plpgsql' ],
 	'fails if language already exists');
 
-psql 'postgres', 'DROP EXTENSION plpgsql';
+psql $node->connstr('postgres'), 'DROP EXTENSION plpgsql';
 issues_sql_like(
-	[ 'createlang', 'plpgsql', 'postgres' ],
+	$node,
+	[ 'createlang', 'plpgsql' ],
 	qr/statement: CREATE EXTENSION "plpgsql"/,
 	'SQL CREATE EXTENSION run');
 
-command_like([ 'createlang', '--list', 'postgres' ],
+command_like([ 'createlang', '--list' ],
 	qr/plpgsql/, 'list output');
diff --git a/src/bin/scripts/t/040_createuser.pl b/src/bin/scripts/t/040_createuser.pl
index 4d44e14..aa93247 100644
--- a/src/bin/scripts/t/040_createuser.pl
+++ b/src/bin/scripts/t/040_createuser.pl
@@ -7,22 +7,30 @@ program_help_ok('createuser');
 program_version_ok('createuser');
 program_options_handling_ok('createuser');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
+
+$ENV{PGDATABASE} = 'postgres';
+$ENV{PGPORT} = $node->port;
 
 issues_sql_like(
+	$node,
 	[ 'createuser', 'user1' ],
 qr/statement: CREATE ROLE user1 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN;/,
 	'SQL CREATE USER run');
 issues_sql_like(
+	$node,
 	[ 'createuser', '-L', 'role1' ],
 qr/statement: CREATE ROLE role1 NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOLOGIN;/,
 	'create a non-login role');
 issues_sql_like(
+	$node,
 	[ 'createuser', '-r', 'user2' ],
 qr/statement: CREATE ROLE user2 NOSUPERUSER NOCREATEDB CREATEROLE INHERIT LOGIN;/,
 	'create a CREATEROLE user');
 issues_sql_like(
+	$node,
 	[ 'createuser', '-s', 'user3' ],
 qr/statement: CREATE ROLE user3 SUPERUSER CREATEDB CREATEROLE INHERIT LOGIN;/,
 	'create a superuser');
diff --git a/src/bin/scripts/t/050_dropdb.pl b/src/bin/scripts/t/050_dropdb.pl
index 3065e50..c59319b 100644
--- a/src/bin/scripts/t/050_dropdb.pl
+++ b/src/bin/scripts/t/050_dropdb.pl
@@ -7,11 +7,15 @@ program_help_ok('dropdb');
 program_version_ok('dropdb');
 program_options_handling_ok('dropdb');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
 
-psql 'postgres', 'CREATE DATABASE foobar1';
+$ENV{PGPORT} = $node->port;
+
+psql $node->connstr('postgres'), 'CREATE DATABASE foobar1';
 issues_sql_like(
+	$node,
 	[ 'dropdb', 'foobar1' ],
 	qr/statement: DROP DATABASE foobar1/,
 	'SQL DROP DATABASE run');
diff --git a/src/bin/scripts/t/060_droplang.pl b/src/bin/scripts/t/060_droplang.pl
index 6a21d7e..b59ac93 100644
--- a/src/bin/scripts/t/060_droplang.pl
+++ b/src/bin/scripts/t/060_droplang.pl
@@ -7,10 +7,14 @@ program_help_ok('droplang');
 program_version_ok('droplang');
 program_options_handling_ok('droplang');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
+
+$ENV{PGPORT} = $node->port;
 
 issues_sql_like(
+	$node,
 	[ 'droplang', 'plpgsql', 'postgres' ],
 	qr/statement: DROP EXTENSION "plpgsql"/,
 	'SQL DROP EXTENSION run');
diff --git a/src/bin/scripts/t/070_dropuser.pl b/src/bin/scripts/t/070_dropuser.pl
index bbb3b79..9331f3e 100644
--- a/src/bin/scripts/t/070_dropuser.pl
+++ b/src/bin/scripts/t/070_dropuser.pl
@@ -7,11 +7,15 @@ program_help_ok('dropuser');
 program_version_ok('dropuser');
 program_options_handling_ok('dropuser');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
+
+$ENV{PGPORT} = $node->port;
 
 psql 'postgres', 'CREATE ROLE foobar1';
 issues_sql_like(
+	$node,
 	[ 'dropuser', 'foobar1' ],
 	qr/statement: DROP ROLE foobar1/,
 	'SQL DROP ROLE run');
diff --git a/src/bin/scripts/t/080_pg_isready.pl b/src/bin/scripts/t/080_pg_isready.pl
index f432505..73429f4 100644
--- a/src/bin/scripts/t/080_pg_isready.pl
+++ b/src/bin/scripts/t/080_pg_isready.pl
@@ -9,7 +9,10 @@ program_options_handling_ok('pg_isready');
 
 command_fails(['pg_isready'], 'fails with no server running');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
+
+$ENV{PGPORT} = $node->port;
 
 command_ok(['pg_isready'], 'succeeds with server running');
diff --git a/src/bin/scripts/t/090_reindexdb.pl b/src/bin/scripts/t/090_reindexdb.pl
index 42628c2..2d9fee1 100644
--- a/src/bin/scripts/t/090_reindexdb.pl
+++ b/src/bin/scripts/t/090_reindexdb.pl
@@ -7,12 +7,15 @@ program_help_ok('reindexdb');
 program_version_ok('reindexdb');
 program_options_handling_ok('reindexdb');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
 
+$ENV{PGPORT} = $node->port;
 $ENV{PGOPTIONS} = '--client-min-messages=WARNING';
 
 issues_sql_like(
+	$node,
 	[ 'reindexdb', 'postgres' ],
 	qr/statement: REINDEX DATABASE postgres;/,
 	'SQL REINDEX run');
@@ -20,22 +23,27 @@ issues_sql_like(
 psql 'postgres',
   'CREATE TABLE test1 (a int); CREATE INDEX test1x ON test1 (a);';
 issues_sql_like(
+	$node,
 	[ 'reindexdb', '-t', 'test1', 'postgres' ],
 	qr/statement: REINDEX TABLE test1;/,
 	'reindex specific table');
 issues_sql_like(
+	$node,
 	[ 'reindexdb', '-i', 'test1x', 'postgres' ],
 	qr/statement: REINDEX INDEX test1x;/,
 	'reindex specific index');
 issues_sql_like(
+	$node,
 	[ 'reindexdb', '-S', 'pg_catalog', 'postgres' ],
 	qr/statement: REINDEX SCHEMA pg_catalog;/,
 	'reindex specific schema');
 issues_sql_like(
+	$node,
 	[ 'reindexdb', '-s', 'postgres' ],
 	qr/statement: REINDEX SYSTEM postgres;/,
 	'reindex system tables');
 issues_sql_like(
+	$node,
 	[ 'reindexdb', '-v', '-t', 'test1', 'postgres' ],
 	qr/statement: REINDEX \(VERBOSE\) TABLE test1;/,
 	'reindex with verbose output');
diff --git a/src/bin/scripts/t/091_reindexdb_all.pl b/src/bin/scripts/t/091_reindexdb_all.pl
index ffadf29..f6643d5 100644
--- a/src/bin/scripts/t/091_reindexdb_all.pl
+++ b/src/bin/scripts/t/091_reindexdb_all.pl
@@ -3,12 +3,15 @@ use warnings;
 use TestLib;
 use Test::More tests => 2;
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
 
+$ENV{PGPORT} = $node->port;
 $ENV{PGOPTIONS} = '--client-min-messages=WARNING';
 
 issues_sql_like(
+	$node,
 	[ 'reindexdb', '-a' ],
 	qr/statement: REINDEX.*statement: REINDEX/s,
 	'reindex all databases');
diff --git a/src/bin/scripts/t/100_vacuumdb.pl b/src/bin/scripts/t/100_vacuumdb.pl
index ac160ba..6329b34 100644
--- a/src/bin/scripts/t/100_vacuumdb.pl
+++ b/src/bin/scripts/t/100_vacuumdb.pl
@@ -7,26 +7,34 @@ program_help_ok('vacuumdb');
 program_version_ok('vacuumdb');
 program_options_handling_ok('vacuumdb');
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
+
+$ENV{PGPORT} = $node->port;
 
 issues_sql_like(
+	$node,
 	[ 'vacuumdb', 'postgres' ],
 	qr/statement: VACUUM;/,
 	'SQL VACUUM run');
 issues_sql_like(
+	$node,
 	[ 'vacuumdb', '-f', 'postgres' ],
 	qr/statement: VACUUM \(FULL\);/,
 	'vacuumdb -f');
 issues_sql_like(
+	$node,
 	[ 'vacuumdb', '-F', 'postgres' ],
 	qr/statement: VACUUM \(FREEZE\);/,
 	'vacuumdb -F');
 issues_sql_like(
+	$node,
 	[ 'vacuumdb', '-z', 'postgres' ],
 	qr/statement: VACUUM \(ANALYZE\);/,
 	'vacuumdb -z');
 issues_sql_like(
+	$node,
 	[ 'vacuumdb', '-Z', 'postgres' ],
 	qr/statement: ANALYZE;/,
 	'vacuumdb -Z');
diff --git a/src/bin/scripts/t/101_vacuumdb_all.pl b/src/bin/scripts/t/101_vacuumdb_all.pl
index e90f321..cde6b3c 100644
--- a/src/bin/scripts/t/101_vacuumdb_all.pl
+++ b/src/bin/scripts/t/101_vacuumdb_all.pl
@@ -3,10 +3,14 @@ use warnings;
 use TestLib;
 use Test::More tests => 2;
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
+
+$ENV{PGPORT} = $node->port;
 
 issues_sql_like(
+	$node,
 	[ 'vacuumdb', '-a' ],
 	qr/statement: VACUUM.*statement: VACUUM/s,
 	'vacuum all databases');
diff --git a/src/bin/scripts/t/102_vacuumdb_stages.pl b/src/bin/scripts/t/102_vacuumdb_stages.pl
index 57b980e..c7ba021 100644
--- a/src/bin/scripts/t/102_vacuumdb_stages.pl
+++ b/src/bin/scripts/t/102_vacuumdb_stages.pl
@@ -3,10 +3,14 @@ use warnings;
 use TestLib;
 use Test::More tests => 4;
 
-my $tempdir = tempdir;
-start_test_server $tempdir;
+my $node = get_new_node();
+$node->init;
+$node->start;
+
+$ENV{PGPORT} = $node->port;
 
 issues_sql_like(
+	$node,
 	[ 'vacuumdb', '--analyze-in-stages', 'postgres' ],
 qr/.*statement:\ SET\ default_statistics_target=1;\ SET\ vacuum_cost_delay=0;
                    .*statement:\ ANALYZE.*
@@ -18,6 +22,7 @@ qr/.*statement:\ SET\ default_statistics_target=1;\ SET\ vacuum_cost_delay=0;
 
 
 issues_sql_like(
+	$node,
 	[ 'vacuumdb', '--analyze-in-stages', '--all' ],
 qr/.*statement:\ SET\ default_statistics_target=1;\ SET\ vacuum_cost_delay=0;
                    .*statement:\ ANALYZE.*
diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm
new file mode 100644
index 0000000..e1d5353
--- /dev/null
+++ b/src/test/perl/PostgresNode.pm
@@ -0,0 +1,251 @@
+# PostgresNode, class representing a data directory and postmaster.
+#
+# This contains a basic set of routines able to work on a PostgreSQL node,
+# allowing to start, stop, backup and initialize it with various options.
+
+package PostgresNode;
+
+use strict;
+use warnings;
+
+use RecursiveCopy;
+use TestBase;
+use Test::More;
+
+sub new
+{
+	my $class  = shift;
+	my $pghost = shift;
+	my $pgport = shift;
+	my $self   = {
+		_port     => $pgport,
+		_host     => $pghost,
+		_basedir  => TestBase::tempdir,
+		_applname => "node_$pgport",
+		_logfile  => "$TestBase::log_path/node_$pgport.log" };
+
+	bless $self, $class;
+	$self->dump_info;
+
+	return $self;
+}
+
+sub port
+{
+	my ($self) = @_;
+	return $self->{_port};
+}
+
+sub host
+{
+	my ($self) = @_;
+	return $self->{_host};
+}
+
+sub basedir
+{
+	my ($self) = @_;
+	return $self->{_basedir};
+}
+
+sub applname
+{
+	my ($self) = @_;
+	return $self->{_applname};
+}
+
+sub logfile
+{
+	my ($self) = @_;
+	return $self->{_logfile};
+}
+
+sub connstr
+{
+	my ($self, $dbname) = @_;
+	my $pgport = $self->port;
+	my $pghost = $self->host;
+	if (!defined($dbname))
+	{
+		return "port=$pgport host=$pghost";
+	}
+	return "port=$pgport host=$pghost dbname=$dbname";
+}
+
+sub data_dir
+{
+	my ($self) = @_;
+	my $res = $self->basedir;
+	return "$res/pgdata";
+}
+
+sub archive_dir
+{
+	my ($self) = @_;
+	my $basedir = $self->basedir;
+	return "$basedir/archives";
+}
+
+sub backup_dir
+{
+	my ($self) = @_;
+	my $basedir = $self->basedir;
+	return "$basedir/backup";
+}
+
+# Dump node information
+sub dump_info
+{
+	my ($self) = @_;
+	print 'Data directory: ' . $self->data_dir . "\n";
+	print 'Backup directory: ' . $self->backup_dir . "\n";
+	print 'Archive directory: ' . $self->archive_dir . "\n";
+	print 'Connection string: ' . $self->connstr . "\n";
+	print 'Application name: ' . $self->applname . "\n";
+	print 'Log file: ' . $self->logfile . "\n";
+}
+
+sub set_replication_conf
+{
+	my ($self) = @_;
+	my $pgdata = $self->data_dir;
+
+	open my $hba, ">>$pgdata/pg_hba.conf";
+	print $hba "\n# Allow replication (set up by PostgresNode.pm)\n";
+	if (!$windows_os)
+	{
+		print $hba "local replication all trust\n";
+	}
+	else
+	{
+		print $hba
+"host replication all 127.0.0.1/32 sspi include_realm=1 map=regress\n";
+	}
+	close $hba;
+}
+
+# Initialize a new cluster for testing.
+#
+# Authentication is set up so that only the current OS user can access the
+# cluster. On Unix, we use Unix domain socket connections, with the socket in
+# a directory that's only accessible to the current user to ensure that.
+# On Windows, we use SSPI authentication to ensure the same (by pg_regress
+# --config-auth).
+sub init
+{
+	my ($self, %params) = @_;
+	my $port   = $self->port;
+	my $pgdata = $self->data_dir;
+	my $host   = $self->host;
+
+	$params{hba_permit_replication} = 1 if (!defined($params{hba_permit_replication}));
+
+	mkdir $self->backup_dir;
+	mkdir $self->archive_dir;
+
+	system_or_bail('initdb', '-D', $pgdata, '-A', 'trust', '-N');
+	system_or_bail($ENV{PG_REGRESS}, '--config-auth', $pgdata);
+
+	open my $conf, ">>$pgdata/postgresql.conf";
+	print $conf "\n# Added by TestLib.pm)\n";
+	print $conf "fsync = off\n";
+	print $conf "log_statement = all\n";
+	print $conf "port = $port\n";
+	if ($windows_os)
+	{
+		print $conf "listen_addresses = '$host'\n";
+	}
+	else
+	{
+		print $conf "unix_socket_directories = '$host'\n";
+		print $conf "listen_addresses = ''\n";
+	}
+	close $conf;
+
+	$self->set_replication_conf if ($params{hba_permit_replication});
+}
+
+sub append_conf
+{
+	my ($self, $filename, $str) = @_;
+
+	my $conffile = $self->data_dir . '/' . $filename;
+
+	append_to_file($conffile, $str);
+}
+
+sub backup
+{
+	my ($self, $backup_name) = @_;
+	my $backup_path = $self->backup_dir . '/' . $backup_name;
+	my $port        = $self->port;
+
+	print "# Taking backup $backup_name from node with port $port\n";
+	system_or_bail("pg_basebackup -D $backup_path -p $port -x");
+	print "# Backup finished\n";
+}
+
+sub init_from_backup
+{
+	my ($self, $root_node, $backup_name) = @_;
+	my $backup_path = $root_node->backup_dir . '/' . $backup_name;
+	my $port        = $self->port;
+	my $root_port   = $root_node->port;
+
+	print
+"Initializing node $port from backup \"$backup_name\" of node $root_port\n";
+	die "Backup $backup_path does not exist" unless -d $backup_path;
+
+	mkdir $self->backup_dir;
+	mkdir $self->archive_dir;
+
+	my $data_path = $self->data_dir;
+	rmdir($data_path);
+	RecursiveCopy::copypath($backup_path, $data_path);
+	chmod(0700, $data_path);
+
+	# Base configuration for this node
+	$self->append_conf('postgresql.conf',
+		qq(
+port = $port
+));
+	$self->set_replication_conf;
+}
+
+sub start
+{
+	my ($self) = @_;
+	my $port   = $self->port;
+	my $pgdata = $self->data_dir;
+	print("### Starting test server in $pgdata\n");
+	my $ret = system_log('pg_ctl', '-w', '-D', $self->data_dir,
+		'-l', $self->logfile, 'start');
+
+	if ($ret != 0)
+	{
+		print "# pg_ctl failed; logfile:\n";
+		print slurp_file($self->logfile);
+		BAIL_OUT("pg_ctl failed");
+	}
+}
+
+sub stop
+{
+	my ($self, $mode) = @_;
+	my $port   = $self->port;
+	my $pgdata = $self->data_dir;
+	$mode = 'fast' if (!defined($mode));
+	print "### Stopping node in $pgdata with port $port using mode $mode\n";
+	system_log('pg_ctl', '-D', $pgdata, '-m', $mode, 'stop');
+}
+
+sub restart
+{
+	my ($self)  = @_;
+	my $port    = $self->port;
+	my $pgdata  = $self->data_dir;
+	my $logfile = $self->logfile;
+	system_log('pg_ctl', '-D', $pgdata, '-w', '-l', $logfile, 'restart');
+}
+
+1;
diff --git a/src/test/perl/RecursiveCopy.pm b/src/test/perl/RecursiveCopy.pm
new file mode 100644
index 0000000..4e58ad3
--- /dev/null
+++ b/src/test/perl/RecursiveCopy.pm
@@ -0,0 +1,42 @@
+# RecursiveCopy, a simple recursive copy implementation
+package RecursiveCopy;
+
+use strict;
+use warnings;
+
+use File::Basename;
+use File::Copy;
+
+sub copypath
+{
+	my $srcpath  = shift;
+	my $destpath = shift;
+
+	die "Cannot operate on symlinks" if -l $srcpath or -l $destpath;
+
+	# This source path is a file, simply copy it to destination with the
+	# same name.
+	die "Destination path $destpath exists as file" if -f $destpath;
+	if (-f $srcpath)
+	{
+		copy($srcpath, $destpath)
+			or die "copy $srcpath -> $destpath failed: $!";
+		return 1;
+	}
+
+	die "Destination needs to be a directory" unless -d $srcpath;
+	mkdir($destpath) or die "mkdir($destpath) failed: $!";
+
+	# Scan existing source directory and recursively copy everything.
+	opendir(my $directory, $srcpath) or die "could not opendir($srcpath): $!";
+	while (my $entry = readdir($directory))
+	{
+		next if ($entry eq '.' || $entry eq '..');
+		RecursiveCopy::copypath("$srcpath/$entry", "$destpath/$entry")
+			or die "copypath $srcpath/$entry -> $destpath/$entry failed";
+	}
+	closedir($directory);
+	return 1;
+}
+
+1;
diff --git a/src/test/perl/TestBase.pm b/src/test/perl/TestBase.pm
new file mode 100644
index 0000000..73c25c1
--- /dev/null
+++ b/src/test/perl/TestBase.pm
@@ -0,0 +1,145 @@
+# testBase, low-level routines and actions regression tests.
+#
+# This module contains a set of routines dedicated to environment setup for
+# a PostgreSQL regression test sun, and includes some low-level routines
+# aimed at controlling command execution and logging. This module should
+# never depend on any other PostgreSQL regression test modules.
+
+package TestBase;
+
+use strict;
+use warnings;
+
+use Config;
+use Exporter 'import';
+use File::Basename;
+use File::Spec;
+use File::Temp ();
+use IPC::Run qw(run);
+use SimpleTee;
+use Test::More;
+
+our @EXPORT = qw(
+  system_or_bail
+  system_log
+  run_log
+  slurp_dir
+  slurp_file
+  append_to_file
+
+  $windows_os
+);
+
+our ($windows_os, $tmp_check, $log_path, $test_logfile);
+
+INIT
+{
+	$windows_os = $Config{osname} eq 'MSWin32' || $Config{osname} eq 'msys';
+
+	# Determine output directories, and create them.  The base path is the
+	# TESTDIR environment variable, which is normally set by the invoking
+	# Makefile.
+	$tmp_check = $ENV{TESTDIR} ? "$ENV{TESTDIR}/tmp_check" : "tmp_check";
+	$log_path = "$tmp_check/log";
+
+	mkdir $tmp_check;
+	mkdir $log_path;
+
+	# Open the test log file, whose name depends on the test name.
+	$test_logfile = basename($0);
+	$test_logfile =~ s/\.[^.]+$//;
+	$test_logfile = "$log_path/regress_log_$test_logfile";
+	open TESTLOG, '>', $test_logfile
+	  or die "could not open STDOUT to logfile \"$test_logfile\": $!";
+
+	# Hijack STDOUT and STDERR to the log file
+	open(ORIG_STDOUT, ">&STDOUT");
+	open(ORIG_STDERR, ">&STDERR");
+	open(STDOUT,      ">&TESTLOG");
+	open(STDERR,      ">&TESTLOG");
+
+	# The test output (ok ...) needs to be printed to the original STDOUT so
+	# that the 'prove' program can parse it, and display it to the user in
+	# real time. But also copy it to the log file, to provide more context
+	# in the log.
+	my $builder = Test::More->builder;
+	my $fh      = $builder->output;
+	tie *$fh, "SimpleTee", *ORIG_STDOUT, *TESTLOG;
+	$fh = $builder->failure_output;
+	tie *$fh, "SimpleTee", *ORIG_STDERR, *TESTLOG;
+
+	# Enable auto-flushing for all the file handles. Stderr and stdout are
+	# redirected to the same file, and buffering causes the lines to appear
+	# in the log in confusing order.
+	autoflush STDOUT 1;
+	autoflush STDERR 1;
+	autoflush TESTLOG 1;
+}
+
+#
+# Helper functions
+#
+sub tempdir
+{
+	return File::Temp::tempdir(
+		'tmp_testXXXX',
+		DIR => $ENV{TESTDIR} || cwd(),
+		CLEANUP => 1);
+}
+
+sub tempdir_short
+{
+	# Use a separate temp dir outside the build tree for the
+	# Unix-domain socket, to avoid file name length issues.
+	return File::Temp::tempdir(CLEANUP => 1);
+}
+
+sub system_log
+{
+	print("# Running: " . join(" ", @_) . "\n");
+	return system(@_);
+}
+
+sub system_or_bail
+{
+	if (system_log(@_) != 0)
+	{
+		BAIL_OUT("system $_[0] failed");
+	}
+}
+
+sub run_log
+{
+	print("# Running: " . join(" ", @{ $_[0] }) . "\n");
+	return run(@_);
+}
+
+sub slurp_dir
+{
+	my ($dir) = @_;
+	opendir(my $dh, $dir)
+	  or die "could not opendir \"$dir\": $!";
+	my @direntries = readdir $dh;
+	closedir $dh;
+	return @direntries;
+}
+
+sub slurp_file
+{
+	local $/;
+	local @ARGV = @_;
+	my $contents = <>;
+	$contents =~ s/\r//g if $Config{osname} eq 'msys';
+	return $contents;
+}
+
+sub append_to_file
+{
+	my ($filename, $str) = @_;
+
+	open my $fh, ">>", $filename or die "could not open \"$filename\": $!";
+	print $fh $str;
+	close $fh;
+}
+
+1;
diff --git a/src/test/perl/TestLib.pm b/src/test/perl/TestLib.pm
index 02533eb..1ed98f2 100644
--- a/src/test/perl/TestLib.pm
+++ b/src/test/perl/TestLib.pm
@@ -4,20 +4,21 @@ use strict;
 use warnings;
 
 use Config;
+use Cwd;
 use Exporter 'import';
+use File::Basename;
+use File::Spec;
+use File::Temp ();
+use IPC::Run qw(run start);
+use PostgresNode;
+use Test::More;
+use TestBase;
+
 our @EXPORT = qw(
-  tempdir
-  tempdir_short
-  standard_initdb
-  configure_hba_for_replication
-  start_test_server
-  restart_test_server
+  get_new_node
+  teardown_node
   psql
-  slurp_dir
-  slurp_file
-  system_or_bail
-  system_log
-  run_log
+  poll_query_until
 
   command_ok
   command_fails
@@ -27,197 +28,91 @@ our @EXPORT = qw(
   program_options_handling_ok
   command_like
   issues_sql_like
-
-  $tmp_check
-  $log_path
-  $windows_os
 );
 
-use Cwd;
-use File::Basename;
-use File::Spec;
-use File::Temp ();
-use IPC::Run qw(run start);
-
-use SimpleTee;
+our ($test_pghost, $last_port_assigned);
+our (@all_nodes,   @active_nodes);
 
-use Test::More;
-
-our $windows_os = $Config{osname} eq 'MSWin32' || $Config{osname} eq 'msys';
-
-# Open log file. For each test, the log file name uses the name of the
-# file launching this module, without the .pl suffix.
-our ($tmp_check, $log_path);
-$tmp_check = $ENV{TESTDIR} ? "$ENV{TESTDIR}/tmp_check" : "tmp_check";
-$log_path = "$tmp_check/log";
-mkdir $tmp_check;
-mkdir $log_path;
-my $test_logfile = basename($0);
-$test_logfile =~ s/\.[^.]+$//;
-$test_logfile = "$log_path/regress_log_$test_logfile";
-open TESTLOG, '>', $test_logfile or die "Cannot open STDOUT to logfile: $!";
-
-# Hijack STDOUT and STDERR to the log file
-open(ORIG_STDOUT, ">&STDOUT");
-open(ORIG_STDERR, ">&STDERR");
-open(STDOUT, ">&TESTLOG");
-open(STDERR, ">&TESTLOG");
-
-# The test output (ok ...) needs to be printed to the original STDOUT so
-# that the 'prove' program can parse it, and display it to the user in
-# real time. But also copy it to the log file, to provide more context
-# in the log.
-my $builder = Test::More->builder;
-my $fh = $builder->output;
-tie *$fh, "SimpleTee", *ORIG_STDOUT, *TESTLOG;
-$fh = $builder->failure_output;
-tie *$fh, "SimpleTee", *ORIG_STDERR, *TESTLOG;
-
-# Enable auto-flushing for all the file handles. Stderr and stdout are
-# redirected to the same file, and buffering causes the lines to appear
-# in the log in confusing order.
-autoflush STDOUT 1;
-autoflush STDERR 1;
-autoflush TESTLOG 1;
-
-# Set to untranslated messages, to be able to compare program output
-# with expected strings.
-delete $ENV{LANGUAGE};
-delete $ENV{LC_ALL};
-$ENV{LC_MESSAGES} = 'C';
-
-delete $ENV{PGCONNECT_TIMEOUT};
-delete $ENV{PGDATA};
-delete $ENV{PGDATABASE};
-delete $ENV{PGHOSTADDR};
-delete $ENV{PGREQUIRESSL};
-delete $ENV{PGSERVICE};
-delete $ENV{PGSSLMODE};
-delete $ENV{PGUSER};
-
-if (!$ENV{PGPORT})
+BEGIN
 {
-	$ENV{PGPORT} = 65432;
+	# Set to untranslated messages, to be able to compare program output
+	# with expected strings.
+	delete $ENV{LANGUAGE};
+	delete $ENV{LC_ALL};
+	$ENV{LC_MESSAGES} = 'C';
+
+	delete $ENV{PGCONNECT_TIMEOUT};
+	delete $ENV{PGDATA};
+	delete $ENV{PGDATABASE};
+	delete $ENV{PGHOSTADDR};
+	delete $ENV{PGREQUIRESSL};
+	delete $ENV{PGSERVICE};
+	delete $ENV{PGSSLMODE};
+	delete $ENV{PGUSER};
+	delete $ENV{PGPORT};
+	delete $ENV{PGHOST};
+
+	# PGHOST is set once and for all through a single series of tests when
+	# this module is loaded.
+	$test_pghost = $windows_os ? "127.0.0.1" : TestBase::tempdir_short();
+	$ENV{PGHOST} = $test_pghost;
+	$ENV{PGDATABASE} = 'postgres';
+
+	# Tracking of last port value assigned to accelerate free port lookup.
+	# XXX: Should this use PG_VERSION_NUM?
+	$last_port_assigned = 90600 % 16384 + 49152;
+
+	# Tracker of active nodes
+	@all_nodes    = ();
+	@active_nodes = ();
 }
 
-$ENV{PGPORT} = int($ENV{PGPORT}) % 65536;
-
-
+# Build a new PostgresNode object, assigning a free port number.
 #
-# Helper functions
-#
-
-
-sub tempdir
+# We also register the node, to avoid the port number from being reused
+# for another node even when this one is not active.
+sub get_new_node
 {
-	return File::Temp::tempdir(
-		'tmp_testXXXX',
-		DIR => $ENV{TESTDIR} || cwd(),
-		CLEANUP => 1);
-}
+	my $found = 0;
+	my $port  = $last_port_assigned;
 
-sub tempdir_short
-{
-
-	# Use a separate temp dir outside the build tree for the
-	# Unix-domain socket, to avoid file name length issues.
-	return File::Temp::tempdir(CLEANUP => 1);
-}
-
-# Initialize a new cluster for testing.
-#
-# The PGHOST environment variable is set to connect to the new cluster.
-#
-# Authentication is set up so that only the current OS user can access the
-# cluster. On Unix, we use Unix domain socket connections, with the socket in
-# a directory that's only accessible to the current user to ensure that.
-# On Windows, we use SSPI authentication to ensure the same (by pg_regress
-# --config-auth).
-sub standard_initdb
-{
-	my $pgdata = shift;
-	system_or_bail('initdb', '-D', "$pgdata", '-A' , 'trust', '-N');
-	system_or_bail($ENV{PG_REGRESS}, '--config-auth', $pgdata);
-
-	my $tempdir_short = tempdir_short;
-
-	open CONF, ">>$pgdata/postgresql.conf";
-	print CONF "\n# Added by TestLib.pm)\n";
-	print CONF "fsync = off\n";
-	if ($windows_os)
+	while ($found == 0)
 	{
-		print CONF "listen_addresses = '127.0.0.1'\n";
+		$port++;
+		print "# Checking for port $port\n";
+		my $devnull = $windows_os ? "nul" : "/dev/null";
+		if (!run_log([ 'pg_isready', '-p', $port ]))
+		{
+			$found = 1;
+
+			# Found a potential candidate port number.  Check first that it is
+			# not included in the list of registered nodes.
+			foreach my $node (@all_nodes)
+			{
+				$found = 0 if ($node->port == $port);
+			}
+		}
 	}
-	else
-	{
-		print CONF "unix_socket_directories = '$tempdir_short'\n";
-		print CONF "listen_addresses = ''\n";
-	}
-	close CONF;
 
-	$ENV{PGHOST}         = $windows_os ? "127.0.0.1" : $tempdir_short;
-}
+	print "# Found free port $port\n";
 
-# Set up the cluster to allow replication connections, in the same way that
-# standard_initdb does for normal connections.
-sub configure_hba_for_replication
-{
-	my $pgdata = shift;
+	# Lock port number found by creating a new node
+	my $node = new PostgresNode($test_pghost, $port);
 
-	open HBA, ">>$pgdata/pg_hba.conf";
-	print HBA "\n# Allow replication (set up by TestLib.pm)\n";
-	if (! $windows_os)
-	{
-		print HBA "local replication all trust\n";
-	}
-	else
-	{
-		print HBA "host replication all 127.0.0.1/32 sspi include_realm=1 map=regress\n";
-	}
-	close HBA;
-}
-
-my ($test_server_datadir, $test_server_logfile);
-
-
-# Initialize a new cluster for testing in given directory, and start it.
-sub start_test_server
-{
-	my ($tempdir) = @_;
-	my $ret;
+	# Add node to list of nodes currently in use
+	push(@all_nodes,    $node);
+	push(@active_nodes, $node);
+	$last_port_assigned = $port;
 
-	print("### Starting test server in $tempdir\n");
-	standard_initdb "$tempdir/pgdata";
-
-	$ret = system_log('pg_ctl', '-D', "$tempdir/pgdata", '-w', '-l',
-	  "$log_path/postmaster.log", '-o', "--log-statement=all",
-	  'start');
-
-	if ($ret != 0)
-	{
-		print "# pg_ctl failed; logfile:\n";
-		system('cat', "$log_path/postmaster.log");
-		BAIL_OUT("pg_ctl failed");
-	}
-
-	$test_server_datadir = "$tempdir/pgdata";
-	$test_server_logfile = "$log_path/postmaster.log";
+	return $node;
 }
 
-sub restart_test_server
+sub teardown_node
 {
-	print("### Restarting test server\n");
-	system_log('pg_ctl', '-D', $test_server_datadir, '-w', '-l',
-	  $test_server_logfile, 'restart');
-}
+	my $node = shift;
 
-END
-{
-	if ($test_server_datadir)
-	{
-		system_log('pg_ctl', '-D', $test_server_datadir, '-m',
-		  'immediate', 'stop');
-	}
+	$node->stop('immediate');
+	@active_nodes = grep { $_ ne $node } @active_nodes;
 }
 
 sub psql
@@ -225,56 +120,55 @@ sub psql
 	my ($dbname, $sql) = @_;
 	my ($stdout, $stderr);
 	print("# Running SQL command: $sql\n");
-	run [ 'psql', '-X', '-A', '-t', '-q', '-d', $dbname, '-f', '-' ], '<', \$sql, '>', \$stdout, '2>', \$stderr or die;
+	run [ 'psql', '-X', '-A', '-t', '-q', '-d', $dbname, '-f', '-' ],
+	  '<', \$sql, '>', \$stdout, '2>', \$stderr
+	  or die;
+	if ($stderr ne "")
+	{
+		print "#### Begin standard error\n";
+		print $stderr;
+		print "#### End standard error\n";
+	}
 	chomp $stdout;
 	$stdout =~ s/\r//g if $Config{osname} eq 'msys';
 	return $stdout;
 }
 
-sub slurp_dir
+# Run a query once a second, until it returns 't' (i.e. SQL boolean true).
+sub poll_query_until
 {
-	my ($dir) = @_;
-	opendir(my $dh, $dir) or die;
-	my @direntries = readdir $dh;
-	closedir $dh;
-	return @direntries;
-}
+	my ($node, $query) = @_;
 
-sub slurp_file
-{
-	local $/;
-	local @ARGV = @_;
-	my $contents = <>;
-	$contents =~ s/\r//g if $Config{osname} eq 'msys';
-	return $contents;
-}
+	my $max_attempts = 30;
+	my $attempts     = 0;
+	my ($stdout, $stderr);
 
-sub system_or_bail
-{
-	if (system_log(@_) != 0)
+	while ($attempts < $max_attempts)
 	{
-		BAIL_OUT("system $_[0] failed: $?");
+		my $cmd = [ 'psql', '-At', '-c', $query, '-d', $node->connstr() ];
+		my $result = run $cmd, '>', \$stdout, '2>', \$stderr;
+
+		chomp($stdout);
+		$stdout =~ s/\r//g if $Config{osname} eq 'msys';
+		if ($stdout eq "t")
+		{
+			return 1;
+		}
+
+		# Wait a second before retrying.
+		sleep 1;
+		$attempts++;
 	}
-}
 
-sub system_log
-{
-	print("# Running: " . join(" ", @_) ."\n");
-	return system(@_);
-}
-
-sub run_log
-{
-	print("# Running: " . join(" ", @{$_[0]}) ."\n");
-	return run (@_);
+	# The query result didn't change in 30 seconds. Give up. Print the stderr
+	# from the last attempt, hopefully that's useful for debugging.
+	diag $stderr;
+	return 0;
 }
 
-
 #
 # Test functions
 #
-
-
 sub command_ok
 {
 	my ($cmd, $test_name) = @_;
@@ -292,7 +186,7 @@ sub command_fails
 sub command_exit_is
 {
 	my ($cmd, $expected, $test_name) = @_;
-	print("# Running: " . join(" ", @{$cmd}) ."\n");
+	print("# Running: " . join(" ", @{$cmd}) . "\n");
 	my $h = start $cmd;
 	$h->finish();
 
@@ -303,8 +197,10 @@ sub command_exit_is
 	# assuming the Unix convention, which will always return 0 on Windows as
 	# long as the process was not terminated by an exception. To work around
 	# that, use $h->full_result on Windows instead.
-	my $result = ($Config{osname} eq "MSWin32") ?
-		($h->full_results)[0] : $h->result(0);
+	my $result =
+	    ($Config{osname} eq "MSWin32")
+	  ? ($h->full_results)[0]
+	  : $h->result(0);
 	is($result, $expected, $test_name);
 }
 
@@ -335,8 +231,8 @@ sub program_options_handling_ok
 	my ($cmd) = @_;
 	my ($stdout, $stderr);
 	print("# Running: $cmd --not-a-valid-option\n");
-	my $result = run [ $cmd, '--not-a-valid-option' ], '>', \$stdout, '2>',
-	  \$stderr;
+	my $result = run [ $cmd, '--not-a-valid-option' ], '>', \$stdout,
+	  '2>', \$stderr;
 	ok(!$result, "$cmd with invalid option nonzero exit code");
 	isnt($stderr, '', "$cmd with invalid option prints error message");
 }
@@ -354,11 +250,11 @@ sub command_like
 
 sub issues_sql_like
 {
-	my ($cmd, $expected_sql, $test_name) = @_;
-	truncate $test_server_logfile, 0;
+	my ($node, $cmd, $expected_sql, $test_name) = @_;
+	truncate $node->logfile, 0;
 	my $result = run_log($cmd);
 	ok($result, "@$cmd exit code 0");
-	my $log = slurp_file($test_server_logfile);
+	my $log = slurp_file($node->logfile);
 	like($log, $expected_sql, "$test_name: SQL found in server log");
 }
 
diff --git a/src/test/ssl/ServerSetup.pm b/src/test/ssl/ServerSetup.pm
index a6c77b5..c6b9f9b 100644
--- a/src/test/ssl/ServerSetup.pm
+++ b/src/test/ssl/ServerSetup.pm
@@ -18,6 +18,7 @@ package ServerSetup;
 
 use strict;
 use warnings;
+use PostgresNode;
 use TestLib;
 use File::Basename;
 use File::Copy;
@@ -45,7 +46,7 @@ sub copy_files
 
 sub configure_test_server_for_ssl
 {
-	my $tempdir    = $_[0];
+	my $pgdata     = $_[0];
 	my $serverhost = $_[1];
 
 	# Create test users and databases
@@ -55,7 +56,7 @@ sub configure_test_server_for_ssl
 	psql 'postgres', "CREATE DATABASE certdb";
 
 	# enable logging etc.
-	open CONF, ">>$tempdir/pgdata/postgresql.conf";
+	open CONF, ">>$pgdata/postgresql.conf";
 	print CONF "fsync=off\n";
 	print CONF "log_connections=on\n";
 	print CONF "log_hostname=on\n";
@@ -68,17 +69,17 @@ sub configure_test_server_for_ssl
 	close CONF;
 
 # Copy all server certificates and keys, and client root cert, to the data dir
-	copy_files("ssl/server-*.crt", "$tempdir/pgdata");
-	copy_files("ssl/server-*.key", "$tempdir/pgdata");
-	chmod(0600, glob "$tempdir/pgdata/server-*.key") or die $!;
-	copy_files("ssl/root+client_ca.crt", "$tempdir/pgdata");
-	copy_files("ssl/root+client.crl",    "$tempdir/pgdata");
+	copy_files("ssl/server-*.crt", $pgdata);
+	copy_files("ssl/server-*.key", $pgdata);
+	chmod(0600, glob "$pgdata/server-*.key") or die $!;
+	copy_files("ssl/root+client_ca.crt", $pgdata);
+	copy_files("ssl/root+client.crl",    $pgdata);
 
   # Only accept SSL connections from localhost. Our tests don't depend on this
   # but seems best to keep it as narrow as possible for security reasons.
   #
   # When connecting to certdb, also check the client certificate.
-	open HBA, ">$tempdir/pgdata/pg_hba.conf";
+	open HBA, ">$pgdata/pg_hba.conf";
 	print HBA
 "# TYPE  DATABASE        USER            ADDRESS                 METHOD\n";
 	print HBA
@@ -96,12 +97,13 @@ sub configure_test_server_for_ssl
 # the server so that the configuration takes effect.
 sub switch_server_cert
 {
-	my $tempdir  = $_[0];
+	my $node     = $_[0];
 	my $certfile = $_[1];
+	my $pgdata   = $node->data_dir;
 
 	diag "Restarting server with certfile \"$certfile\"...";
 
-	open SSLCONF, ">$tempdir/pgdata/sslconfig.conf";
+	open SSLCONF, ">$pgdata/sslconfig.conf";
 	print SSLCONF "ssl=on\n";
 	print SSLCONF "ssl_ca_file='root+client_ca.crt'\n";
 	print SSLCONF "ssl_cert_file='$certfile.crt'\n";
@@ -110,5 +112,5 @@ sub switch_server_cert
 	close SSLCONF;
 
 	# Stop and restart server to reload the new config.
-	restart_test_server();
+	$node->restart;
 }
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 0d6f339..5fd936b 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -1,5 +1,7 @@
 use strict;
 use warnings;
+use PostgresNode;
+use TestBase;
 use TestLib;
 use Test::More tests => 38;
 use ServerSetup;
@@ -25,8 +27,6 @@ BEGIN
 # postgresql-ssl-regression.test.
 my $SERVERHOSTADDR = '127.0.0.1';
 
-my $tempdir = TestLib::tempdir;
-
 # Define a couple of helper functions to test connecting to the server.
 
 my $common_connstr;
@@ -74,10 +74,16 @@ chmod 0600, "ssl/client.key";
 
 #### Part 0. Set up the server.
 
-diag "setting up data directory in \"$tempdir\"...";
-start_test_server($tempdir);
-configure_test_server_for_ssl($tempdir, $SERVERHOSTADDR);
-switch_server_cert($tempdir, 'server-cn-only');
+diag "setting up data directory...";
+my $node = get_new_node();
+$node->init;
+# PGHOST is enforced here to set up the node, subsequent connections
+# will use a dedicated connection string.
+$ENV{PGHOST} = $node->host;
+$ENV{PGPORT} = $node->port;
+$node->start;
+configure_test_server_for_ssl($node->data_dir, $SERVERHOSTADDR);
+switch_server_cert($node, 'server-cn-only');
 
 ### Part 1. Run client-side tests.
 ###
@@ -150,7 +156,7 @@ test_connect_ok("sslmode=verify-ca host=wronghost.test");
 test_connect_fails("sslmode=verify-full host=wronghost.test");
 
 # Test Subject Alternative Names.
-switch_server_cert($tempdir, 'server-multiple-alt-names');
+switch_server_cert($node, 'server-multiple-alt-names');
 
 diag "test hostname matching with X509 Subject Alternative Names";
 $common_connstr =
@@ -165,7 +171,7 @@ test_connect_fails("host=deep.subdomain.wildcard.pg-ssltest.test");
 
 # Test certificate with a single Subject Alternative Name. (this gives a
 # slightly different error message, that's all)
-switch_server_cert($tempdir, 'server-single-alt-name');
+switch_server_cert($node, 'server-single-alt-name');
 
 diag "test hostname matching with a single X509 Subject Alternative Name";
 $common_connstr =
@@ -178,7 +184,7 @@ test_connect_fails("host=deep.subdomain.wildcard.pg-ssltest.test");
 
 # Test server certificate with a CN and SANs. Per RFCs 2818 and 6125, the CN
 # should be ignored when the certificate has both.
-switch_server_cert($tempdir, 'server-cn-and-alt-names');
+switch_server_cert($node, 'server-cn-and-alt-names');
 
 diag "test certificate with both a CN and SANs";
 $common_connstr =
@@ -190,7 +196,7 @@ test_connect_fails("host=common-name.pg-ssltest.test");
 
 # Finally, test a server certificate that has no CN or SANs. Of course, that's
 # not a very sensible certificate, but libpq should handle it gracefully.
-switch_server_cert($tempdir, 'server-no-names');
+switch_server_cert($node, 'server-no-names');
 $common_connstr =
 "user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
 
@@ -199,7 +205,7 @@ test_connect_fails("sslmode=verify-full host=common-name.pg-ssltest.test");
 
 # Test that the CRL works
 diag "Testing client-side CRL";
-switch_server_cert($tempdir, 'server-revoked');
+switch_server_cert($node, 'server-revoked');
 
 $common_connstr =
 "user=ssltestuser dbname=trustdb sslcert=invalid hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
@@ -233,7 +239,3 @@ test_connect_fails(
 test_connect_fails(
 "user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked.key"
 );
-
-
-# All done! Save the log, before the temporary installation is deleted
-copy("$tempdir/client-log", "./client-log");
-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Reply via email to