summaryrefslogtreecommitdiffstats
path: root/t
diff options
context:
space:
mode:
Diffstat (limited to 't')
-rw-r--r--t/001_packages.t87
-rw-r--r--t/002_existing_clusters.t11
-rw-r--r--t/003_alternatives.t37
-rw-r--r--t/005_PgCommon.t311
-rw-r--r--t/006_next_free_port.t49
-rw-r--r--t/007_pg_conftool.t85
-rw-r--r--t/010_defaultport_cluster.t33
-rw-r--r--t/015_start_stop.t174
-rw-r--r--t/020_create_sql_remove.t452
-rw-r--r--t/021_pg_renamecluster.t43
-rw-r--r--t/022_recovery.t54
-rw-r--r--t/025_logging.t99
-rw-r--r--t/030_errors.t336
-rw-r--r--t/031_errors_disk_full.t86
-rw-r--r--t/032_ssl_key_permissions.t60
-rw-r--r--t/040_upgrade.t272
-rw-r--r--t/041_upgrade_custompaths.t51
-rw-r--r--t/042_upgrade_rename.t27
-rw-r--r--t/043_upgrade_ssl_cert.t79
-rw-r--r--t/045_backup.t169
-rw-r--r--t/050_encodings.t117
-rw-r--r--t/052_upgrade_encodings.t98
-rw-r--r--t/060_obsolete_confparams.t83
-rw-r--r--t/070_non_postgres_clusters.t116
-rw-r--r--t/080_start.conf.t145
-rw-r--r--t/085_pg_ctl.conf.t51
-rw-r--r--t/090_multicluster.t286
-rw-r--r--t/110_integrate_cluster.t44
-rw-r--r--t/120_pg_upgradecluster_scripts.t114
-rw-r--r--t/130_nonroot_admin.t50
-rw-r--r--t/135_pg_virtualenv.t35
-rw-r--r--t/140_pg_config.t89
-rw-r--r--t/150_tsearch_stemming.t108
-rw-r--r--t/160_alternate_confroot.t57
-rw-r--r--t/170_extensions.t87
-rw-r--r--t/180_ecpg.t56
-rw-r--r--t/190_pg_buildext.t104
-rw-r--r--t/200_maintscripts.t46
-rw-r--r--t/TestLib.pm270
-rw-r--r--t/bar/debian/control.in8
-rw-r--r--t/bar/debian/pgversions3
-rw-r--r--t/foo/Makefile12
-rw-r--r--t/foo/foo-123/Makefile10
-rw-r--r--t/foo/foo-123/README.md5
-rw-r--r--t/foo/foo-123/expected/foo.out8
-rw-r--r--t/foo/foo-123/expected/upgrade.out14
-rw-r--r--t/foo/foo-123/foo--100--123.sql3
-rw-r--r--t/foo/foo-123/foo--100.sql3
-rw-r--r--t/foo/foo-123/foo--123.sql3
-rw-r--r--t/foo/foo-123/foo.c13
-rw-r--r--t/foo/foo-123/foo.control2
-rw-r--r--t/foo/foo-123/sql/foo.sql5
-rw-r--r--t/foo/foo-123/sql/upgrade.sql7
-rw-r--r--t/template26
54 files changed, 4593 insertions, 0 deletions
diff --git a/t/001_packages.t b/t/001_packages.t
new file mode 100644
index 0000000..697e871
--- /dev/null
+++ b/t/001_packages.t
@@ -0,0 +1,87 @@
+# Check that the necessary packages are installed
+
+use warnings;
+use strict;
+
+use lib 't';
+use TestLib;
+use POSIX qw/setlocale LC_ALL LC_MESSAGES/;
+
+use Test::More tests => $PgCommon::rpm ? (3 + 9*@MAJORS) : (15 + 7*@MAJORS);
+
+ok (-f "/etc/os-release", "/etc/os-release exists");
+my ($os, $osversion) = os_release();
+ok (defined $os, "OS is $os");
+ok (defined $osversion, "OS version is $osversion");
+
+note "PostgreSQL versions installed: @MAJORS\n";
+my $f = $ENV{'PG_FLAVOR'} // '';
+
+if ($PgCommon::rpm) {
+ foreach my $v (@MAJORS) {
+ my $vv = $v;
+ $vv =~ s/\.//;
+
+ ok ((rpm_installed "postgresql$vv$f"), "postgresql$vv$f installed");
+ ok ((rpm_installed "postgresql$vv$f-libs"), "postgresql$vv$f-libs installed");
+ ok ((rpm_installed "postgresql$vv$f-server"), "postgresql$vv$f-server installed");
+ ok ((rpm_installed "postgresql$vv$f-contrib"), "postgresql$vv$f-contrib installed");
+ ok ((rpm_installed "postgresql$vv$f-plperl"), "postgresql$vv$f-plperl installed");
+ SKIP: {
+ skip "No python2 support", 1 unless ($v <= 12);
+ ok ((rpm_installed "postgresql$vv$f-plpython"), "postgresql$vv$f-plpython installed");
+ }
+ ok ((rpm_installed "postgresql$vv$f-plpython3"), "postgresql$vv$f-plpython3 installed");
+ ok ((rpm_installed "postgresql$vv$f-pltcl"), "postgresql$vv$f-pltcl installed");
+ ok ((rpm_installed "postgresql$vv$f-devel"), "postgresql$vv$f-devel installed");
+ }
+ exit;
+}
+
+my $docpkgs = 0;
+foreach my $v (@MAJORS) {
+ ok ((deb_installed "postgresql-$v$f"), "postgresql-$v$f installed");
+ SKIP: {
+ skip "No python2 support", 1 unless ($v <= 11 and $PgCommon::have_python2);
+ ok ((deb_installed "postgresql-plpython-$v$f"), "postgresql-plpython-$v$f installed");
+ }
+ if ($v >= '9.1') {
+ ok ((deb_installed "postgresql-plpython3-$v$f"), "postgresql-plpython3-$v$f installed");
+ } else {
+ pass "no Python 3 package for version $v";
+ }
+ ok ((deb_installed "postgresql-plperl-$v$f"), "postgresql-plperl-$v$f installed");
+ ok ((deb_installed "postgresql-pltcl-$v$f"), "postgresql-pltcl-$v$f installed");
+ ok ((deb_installed "postgresql-server-dev-$v$f"), "postgresql-server-dev-$v$f installed");
+ SKIP: {
+ skip "No postgresql-contrib-$v$f package for version $v", 1 if ($v >= 10);
+ ok ((deb_installed "postgresql-contrib-$v$f"), "postgresql-contrib-$v$f installed");
+ }
+ my $docpkg = "postgresql-doc-$v$f";
+ if (deb_installed $docpkg) {
+ note "$docpkg installed";
+ $docpkgs++;
+ }
+}
+ok $docpkgs, "At least one doc package installed";
+
+ok ((deb_installed 'libecpg-dev'), 'libecpg-dev installed');
+ok ((deb_installed 'procps'), 'procps installed');
+ok ((deb_installed 'netcat-openbsd'), 'netcat-openbsd installed');
+
+ok ((deb_installed 'hunspell-en-us'), 'hunspell-en-us installed');
+
+# check installed locales to fail tests early if they are missing
+ok ((setlocale(LC_MESSAGES, '') =~ /utf8|UTF-8/), 'system has a default UTF-8 locale');
+ok (setlocale (LC_ALL, "ru_RU"), 'locale ru_RU exists');
+ok (setlocale (LC_ALL, "ru_RU.UTF-8"), 'locale ru_RU.UTF-8 exists');
+
+my $key_file = '/etc/ssl/private/ssl-cert-snakeoil.key';
+my $pem_file = '/etc/ssl/certs/ssl-cert-snakeoil.pem';
+ok ((getgrnam('ssl-cert'))[3] =~ /postgres/,
+ 'user postgres in the UNIX group ssl-cert');
+ok (-e $key_file, "$key_file exists");
+is (exec_as ('postgres', "cat $key_file > /dev/null"), 0, "$key_file is readable for postgres");
+ok (-e $pem_file, "$pem_file exists");
+
+# vim: filetype=perl
diff --git a/t/002_existing_clusters.t b/t/002_existing_clusters.t
new file mode 100644
index 0000000..93cc782
--- /dev/null
+++ b/t/002_existing_clusters.t
@@ -0,0 +1,11 @@
+# Check that no clusters and postgres processes are present for this test.
+
+use strict;
+use Test::More tests => 8;
+
+use lib 't';
+use TestLib;
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/003_alternatives.t b/t/003_alternatives.t
new file mode 100644
index 0000000..dd49d6b
--- /dev/null
+++ b/t/003_alternatives.t
@@ -0,0 +1,37 @@
+# Check that alternatives symlinks point to the correct locations
+
+use warnings;
+use strict;
+
+use lib 't';
+use TestLib;
+
+use Test::More tests => $PgCommon::rpm ? 1 : 9;
+
+if ($PgCommon::rpm) {
+ ok "No alternatives test on RedHat";
+ exit;
+}
+
+# server/client link group
+my $newest_version = $ALL_MAJORS[-1];
+note "Newest PG version installed is $newest_version";
+program_ok 0, 'update-alternatives --list psql.1.gz', 0, 'psql.1.gz link group';
+program_ok 0, 'update-alternatives --list postmaster.1.gz', 2, 'postmaster.1.gz link group does not exist'; # removed in pg-common 248
+for my $name (qw(psql pg_dump postgres pg_ctl)) {
+ is readlink "/etc/alternatives/$name.1.gz", "/usr/share/postgresql/$newest_version/man/man1/$name.1.gz", "$name.1.gz alternative";
+}
+
+# doc link group
+my $newest_doc_version = `dpkg -l 'postgresql-doc-[1-9]*' | sed -ne 's/^ii postgresql-doc-\\([0-9.]*\\).*/\\1/p' | sort -g | tail -n 1`;
+note "Newest PG doc version installed is $newest_doc_version";
+SKIP: {
+ skip "No SPI_connect.3.gz link group on 8.x", 3 if ($newest_doc_version < 9.0);
+ chomp $newest_doc_version;
+ program_ok 0, 'update-alternatives --list SPI_connect.3.gz', 0, 'SPI_connect.3.gz link group';
+ for my $name (qw(SPI_connect SPI_exec)) {
+ is readlink "/etc/alternatives/$name.3.gz", "/usr/share/postgresql/$newest_doc_version/man/man3/$name.3.gz", "$name.3.gz alternative";
+ }
+}
+
+# vim: filetype=perl
diff --git a/t/005_PgCommon.t b/t/005_PgCommon.t
new file mode 100644
index 0000000..e1bc337
--- /dev/null
+++ b/t/005_PgCommon.t
@@ -0,0 +1,311 @@
+# Check PgCommon library functions.
+
+use strict;
+
+use File::Temp qw/tempdir/;
+
+use lib '.';
+use PgCommon;
+
+use lib 't';
+use TestLib;
+
+use Test::More tests => 24;
+
+my $tdir = tempdir (CLEANUP => 1);
+$PgCommon::confroot = $tdir;
+
+# test read_pg_hba with valid file
+open P, ">$tdir/pg_hba.conf" or die "Could not create $tdir/pg_hba.conf: $!";
+print P <<EOF;
+# comment
+local all postgres ident sameuser
+
+# TYPE DATABASE USER CIDR-ADDRESS METHOD
+local foo nobody trust
+local foo nobody crypt
+local foo nobody,joe krb5
+local foo,bar nobody ident
+local all +foogrp password
+host \@inc all 127.0.0.1/32 md5
+hostssl all \@inc 192.168.0.0 255.255.0.0 pam
+hostnossl all all 192.168.0.0 255.255.0.0 reject
+EOF
+close P;
+
+my @expected_records = (
+ { 'type' => 'local', 'db' => 'all', 'user' => 'postgres', 'method' => 'ident sameuser' },
+ { 'type' => 'local', 'db' => 'foo', 'user' => 'nobody', 'method' => 'trust' },
+ { 'type' => 'local', 'db' => 'foo', 'user' => 'nobody', 'method' => 'crypt' },
+ { 'type' => 'local', 'db' => 'foo', 'user' => 'nobody,joe', 'method' => 'krb5' },
+ { 'type' => 'local', 'db' => 'foo,bar', 'user' => 'nobody', 'method' => 'ident' },
+ { 'type' => 'local', 'db' => 'all', 'user' => '+foogrp', 'method' => 'password' },
+ { 'type' => 'host', 'db' => '@inc', 'user' => 'all', 'method' => 'md5', 'ip' => '127.0.0.1', 'mask' => '32'},
+ { 'type' => 'hostssl', 'db' => 'all', 'user' => '@inc', 'method' => 'pam', 'ip' => '192.168.0.0', 'mask' => '255.255.0.0'},
+ { 'type' => 'hostnossl', 'db' => 'all', 'user' => 'all', 'method' => 'reject', 'ip' => '192.168.0.0', 'mask' => '255.255.0.0'},
+);
+
+my @hba = read_pg_hba "$tdir/pg_hba.conf";
+foreach my $entry (@hba) {
+ next if $$entry{'type'} eq 'comment';
+ if ($#expected_records < 0) {
+ fail '@expected_records is already empty';
+ next;
+ }
+ my $expected = shift @expected_records;
+ my $parsedstr = '';
+ my $expectedstr = '';
+ foreach my $k (keys %$expected) {
+ $parsedstr .= $k . ':\'' . $$entry{$k} . '\' ';
+ $expectedstr .= $k . ':\'' . $$expected{$k} . '\' ';
+ if ($$expected{$k} ne $$entry{$k}) {
+ fail "mismatch: $expectedstr ne $parsedstr";
+ last;
+ }
+ }
+ pass 'correctly parsed line \'' . $$entry{'line'} . "'";
+}
+
+ok (($#expected_records == -1), '@expected_records has correct number of entries');
+
+# test read_pg_hba with invalid file
+my $invalid_hba = <<EOF;
+foo all all md5
+local all all foo
+host all all foo
+host all all 127.0.0.1/32 foo
+host all all md5
+host all all 127.0.0.1/32 0.0.0.0 md5
+host all all 127.0.0.1 md5
+EOF
+open P, ">$tdir/pg_hba.conf" or die "Could not create $tdir/pg_hba_invalid.conf: $!";
+print P $invalid_hba;
+close P;
+
+@hba = read_pg_hba "$tdir/pg_hba.conf";
+is (scalar (split "\n", $invalid_hba), $#hba+1, 'returned read_pg_hba array has correct number of records');
+foreach my $entry (@hba) {
+ is $$entry{'type'}, undef, 'line \'' . $$entry{'line'} . '\' parsed as invalid';
+}
+
+# test read_conf_file()
+my %conf = PgCommon::read_conf_file '/nonexisting';
+is_deeply \%conf, {}, 'read_conf_file returns empty dict for nonexisting file';
+
+mkdir "$tdir/8.4";
+mkdir "$tdir/8.4/test" or die "mkdir: $!";
+mkdir "$tdir/conf.d" or die "mkdir: $!";
+my $c = "$tdir/8.4/test/foo.conf";
+open F, ">$c" or die "Could not create $c: $!";
+print F <<EOF;
+# test configuration file
+
+# Commented_Int = 12
+# commented_str='foobar'
+# commented_bool off
+# commented_bool2 off # comment
+# commented_bool3 just a comment
+
+#intval = 1
+Intval = 42
+cintval=1 # blabla
+floatval = 1.5e+3
+strval 'hello'
+strval2 'world'
+cstrval = 'bye' # comment
+emptystr = ''
+cemptystr = '' # moo!
+#testpath = '/bin/bad'
+testpath = '/bin/test'
+QuoteStr = 'test ! -f \\'/tmp/%f\\' && echo \\'yes\\''
+EOF
+close F;
+%conf = PgCommon::read_conf_file "$c";
+is_deeply (\%conf, {
+ 'intval' => 42,
+ 'cintval' => 1,
+ 'floatval' => '1.5e+3',
+ 'strval' => 'hello',
+ 'strval2' => 'world',
+ 'cstrval' => 'bye',
+ 'testpath' => '/bin/test',
+ 'emptystr' => '',
+ 'cemptystr' => '',
+ 'quotestr' => "test ! -f '/tmp/%f' && echo 'yes'"
+ }, 'read_conf_file() parsing');
+
+# test read_conf_file() with include directives
+open F, ">$tdir/8.4/test/condinc.conf" or die "Could not create $tdir/condinc.conf: $!";
+print F "condint = 42\n";
+close F;
+
+open F, ">$tdir/bar.conf" or die "Could not create $tdir/bar.conf: $!";
+print F <<EOF;
+# test configuration file
+
+# Commented_Int = 24
+# commented_str = 'notme'
+
+intval = -1
+include '8.4/test/foo.conf' # foo
+include_dir 'conf.d' # bar
+strval = 'howdy'
+include_if_exists '/nonexisting.conf'
+include_if_exists = '8.4/test/condinc.conf'
+EOF
+close F;
+
+open F, ">$tdir/conf.d/sub.conf" or die "Could not create $tdir/conf.d/sub.conf: $!";
+print F <<EOF;
+subvalue = 1
+include = '../relative.conf'
+EOF
+close F;
+
+open F, ">$tdir/relative.conf" or die "Could not create $tdir/relative.conf: $!";
+print F <<EOF;
+relativevalue = 1
+include '$tdir/absolute.conf'
+EOF
+close F;
+
+open F, ">$tdir/absolute.conf" or die "Could not create $tdir/absolute.conf: $!";
+print F <<EOF;
+absolutevalue = 1
+EOF
+close F;
+
+%conf = PgCommon::read_conf_file "$tdir/bar.conf";
+is_deeply (\%conf, {
+ 'intval' => 42,
+ 'cintval' => 1,
+ 'floatval' => '1.5e+3',
+ 'strval' => 'howdy',
+ 'strval2' => 'world',
+ 'cstrval' => 'bye',
+ 'testpath' => '/bin/test',
+ 'emptystr' => '',
+ 'cemptystr' => '',
+ 'quotestr' => "test ! -f '/tmp/%f' && echo 'yes'",
+ 'condint' => 42,
+ 'subvalue' => 1,
+ 'relativevalue' => 1,
+ 'absolutevalue' => 1,
+ }, 'read_conf_file() parsing with include directives');
+
+
+# test set_conf_value()
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'commented_int', '24';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'commented_str', 'new foo';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'commented_bool', 'on';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'commented_bool2', 'on';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'commented_bool3', 'on';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'intval', '39';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'cintval', '5';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'strval', 'Howdy';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'newval', 'NEW!';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'testpath', '/bin/new';
+PgCommon::set_conf_value '8.4', 'test', 'foo.conf', 'include_dir', 'conf.d';
+
+open F, "$c";
+my $conf;
+read F, $conf, 1024;
+close F;
+is ($conf, <<EOF, 'set_conf_value');
+# test configuration file
+
+Commented_Int = 24
+commented_str='new foo'
+commented_bool on
+commented_bool2 on # comment
+# commented_bool3 just a comment
+
+#intval = 1
+Intval = 39
+cintval=5 # blabla
+floatval = 1.5e+3
+strval Howdy
+strval2 'world'
+cstrval = 'bye' # comment
+emptystr = ''
+cemptystr = '' # moo!
+#testpath = '/bin/bad'
+testpath = '/bin/new'
+QuoteStr = 'test ! -f \\'/tmp/%f\\' && echo \\'yes\\''
+commented_bool3 = on
+newval = 'NEW!'
+include_dir = 'conf.d'
+EOF
+
+# test disable_conf_value()
+PgCommon::disable_conf_value '8.4', 'test', 'foo.conf', 'intval', 'ints are out of fashion';
+PgCommon::disable_conf_value '8.4', 'test', 'foo.conf', 'cstrval', 'not used any more';
+PgCommon::disable_conf_value '8.4', 'test', 'foo.conf', 'nonexisting', 'NotMe';
+PgCommon::disable_conf_value '8.4', 'test', 'foo.conf', 'testpath', 'now 2 comments';
+
+open F, "$c";
+read F, $conf, 1024;
+close F;
+is ($conf, <<EOF, 'disable_conf_value');
+# test configuration file
+
+Commented_Int = 24
+commented_str='new foo'
+commented_bool on
+commented_bool2 on # comment
+# commented_bool3 just a comment
+
+#intval = 1
+#Intval = 39 #ints are out of fashion
+cintval=5 # blabla
+floatval = 1.5e+3
+strval Howdy
+strval2 'world'
+#cstrval = 'bye' # comment #not used any more
+emptystr = ''
+cemptystr = '' # moo!
+#testpath = '/bin/bad'
+#testpath = '/bin/new' #now 2 comments
+QuoteStr = 'test ! -f \\'/tmp/%f\\' && echo \\'yes\\''
+commented_bool3 = on
+newval = 'NEW!'
+include_dir = 'conf.d'
+EOF
+
+# test replace_conf_value()
+PgCommon::replace_conf_value '8.4', 'test', 'foo.conf', 'strval',
+ 'renamedstrval', 'newstrval', 'goodbye';
+PgCommon::replace_conf_value '8.4', 'test', 'foo.conf', 'nonexisting',
+ 'renamednonexisting', 'newnonexisting', 'XXX';
+
+open F, "$c";
+read F, $conf, 1024;
+close F;
+is ($conf, <<EOF, 'replace_conf_value');
+# test configuration file
+
+Commented_Int = 24
+commented_str='new foo'
+commented_bool on
+commented_bool2 on # comment
+# commented_bool3 just a comment
+
+#intval = 1
+#Intval = 39 #ints are out of fashion
+cintval=5 # blabla
+floatval = 1.5e+3
+#strval Howdy #renamedstrval
+newstrval = goodbye
+strval2 'world'
+#cstrval = 'bye' # comment #not used any more
+emptystr = ''
+cemptystr = '' # moo!
+#testpath = '/bin/bad'
+#testpath = '/bin/new' #now 2 comments
+QuoteStr = 'test ! -f \\'/tmp/%f\\' && echo \\'yes\\''
+commented_bool3 = on
+newval = 'NEW!'
+include_dir = 'conf.d'
+EOF
+
+# vim: filetype=perl
diff --git a/t/006_next_free_port.t b/t/006_next_free_port.t
new file mode 100644
index 0000000..4a82bf3
--- /dev/null
+++ b/t/006_next_free_port.t
@@ -0,0 +1,49 @@
+# Check PgCommon's next_free_port()
+
+use strict;
+
+use lib '.';
+use PgCommon;
+
+use lib 't';
+use TestLib;
+
+use Test::More tests => 5;
+
+# test next_free_port(). We are intentionally using nc as an external tool,
+# using perl would replicate what next_free_port is doing, and that would
+# be a pointless test.
+use IPC::Open2;
+use Time::HiRes qw(usleep);
+my @pids;
+# no ports open
+is (next_free_port, 5432, 'next_free_port is 5432');
+
+# open a localhost ipv4 socket
+push @pids, open2(\*CHLD_OUT, \*CHLD_IN, qw(nc -4 -l 127.0.0.1 5432));
+usleep 2*$delay;
+is (next_free_port, 5433, 'next_free_port detects localhost ipv4 socket');
+# open a wildcard ipv4 socket
+push @pids, open2(\*CHLD_OUT, \*CHLD_IN, qw(nc -4 -l 5433));
+usleep 2*$delay;
+is (next_free_port, 5434, 'next_free_port detects wildcard ipv4 socket');
+
+SKIP: {
+ $^V =~ /^v(\d+\.\d+)/; # parse perl version
+ skip "perl <= 5.10 does not have proper IPv6 support", 2 if ($1 <= 5.10);
+ skip "skipping IPv6 tests", 2 if ($ENV{SKIP_IPV6});
+
+ # open a localhost ipv6 socket
+ push @pids, open2(\*CHLD_OUT, \*CHLD_IN, qw(nc -6 -l ::1 5434));
+ usleep 2*$delay;
+ is (next_free_port, 5435, 'next_free_port detects localhost ipv6 socket');
+ # open a wildcard ipv6 socket
+ push @pids, open2(\*CHLD_OUT, \*CHLD_IN, qw(nc -6 -l 5435));
+ usleep 2*$delay;
+ is (next_free_port, 5436, 'next_free_port detects wildcard ipv6 socket');
+}
+
+# clean up
+kill 15, @pids;
+
+# vim: filetype=perl
diff --git a/t/007_pg_conftool.t b/t/007_pg_conftool.t
new file mode 100644
index 0000000..e8465ef
--- /dev/null
+++ b/t/007_pg_conftool.t
@@ -0,0 +1,85 @@
+# Test pg_conftool
+
+use strict;
+use warnings;
+
+use Test::More tests => 41;
+use File::Temp qw/tempdir/;
+use lib '.';
+use PgCommon;
+use lib 't';
+use TestLib;
+
+my $tdir = tempdir (CLEANUP => 1);
+$ENV{'PG_CLUSTER_CONF_ROOT'} = $tdir;
+
+open F, "> $tdir/different.conf";
+print F "a = '5'\n";
+print F "#b = '6'\n";
+close F;
+
+note 'test without cluster';
+is_program_out 0, "pg_conftool show all", 1, "Error: No default cluster found\n";
+is_program_out 0, "pg_conftool foo.conf show all", 1, "Error: No default cluster found\n";
+is_program_out 0, "pg_conftool $tdir/different.conf show all", 0, "a = 5\n";
+is_program_out 0, "pg_conftool 9.7 main show all", 1, "Error: Cluster 9.7 main does not exist\n";
+
+my $version = $MAJORS[-1];
+die "Tests past this point need PostgreSQL installed" unless ($version);
+mkdir "$tdir/$version";
+mkdir "$tdir/$version/main";
+
+open F, "> $tdir/$version/main/postgresql.conf";
+print F "a = '1'\n";
+print F "#b = '2'\n";
+close F;
+
+open F, "> $tdir/$version/main/other.conf";
+print F "a = '3'\n";
+print F "#b = '4'\n";
+close F;
+
+sub pgconf {
+ undef $/;
+ open F, "$tdir/$version/main/postgresql.conf";
+ my $f = <F>;
+ close F;
+ return $f;
+}
+
+sub differentconf {
+ undef $/;
+ open F, "$tdir/different.conf";
+ my $f = <F>;
+ close F;
+ return $f;
+}
+
+note 'test show';
+is_program_out 0, "pg_conftool show all", 0, "a = 1\n";
+is_program_out 0, "pg_conftool other.conf show all", 0, "a = 3\n";
+is_program_out 0, "pg_conftool $tdir/different.conf show all", 0, "a = 5\n";
+is_program_out 0, "pg_conftool $version main show all", 0, "a = 1\n";
+is_program_out 0, "pg_conftool $version main other.conf show all", 0, "a = 3\n";
+is_program_out 0, "pg_conftool show a", 0, "a = 1\n";
+is_program_out 0, "pg_conftool -s show a", 0, "1\n";
+
+note 'test set';
+is_program_out 0, "pg_conftool set c 7", 0, "";
+undef $/; # slurp mode
+is pgconf, "a = '1'\n#b = '2'\nc = 7\n", "file contains new setting";
+is_program_out 0, "pg_conftool set a 8", 0, "";
+is pgconf, "a = 8\n#b = '2'\nc = 7\n", "file contains updated setting";
+is_program_out 0, "pg_conftool $tdir/different.conf set a 9", 0, "";
+is differentconf, "a = 9\n#b = '6'\n", "file with path contains updated setting";
+
+note 'test remove';
+is_program_out 0, "pg_conftool remove a", 0, "";
+is pgconf, "#a = 8\n#b = '2'\nc = 7\n", "setting removed from file";
+is_program_out 0, "pg_conftool $tdir/different.conf remove a", 0, "";
+is differentconf, "#a = 9\n#b = '6'\n", "setting removed from file with path";
+
+note 'test edit';
+$ENV{EDITOR} = 'cat';
+is_program_out 0, "pg_conftool edit", 0, "#a = 8\n#b = '2'\nc = 7\n";
+is_program_out 0, "pg_conftool $tdir/different.conf edit", 0, "#a = 9\n#b = '6'\n";
diff --git a/t/010_defaultport_cluster.t b/t/010_defaultport_cluster.t
new file mode 100644
index 0000000..1ba0daa
--- /dev/null
+++ b/t/010_defaultport_cluster.t
@@ -0,0 +1,33 @@
+# We try to call psql with --version and then on localhost. Since there are no
+# clusters, we expect an error message that the connection to port 5432 is
+# refused. This checks that pg_wrapper correctly picks the default port and
+# uses the highest available version.
+
+use strict;
+use Test::More tests => 14;
+
+use lib 't';
+use TestLib;
+
+like_program_out 0, 'psql --version', 0, qr/^psql \(PostgreSQL\) $ALL_MAJORS[-1]/,
+ 'pg_wrapper selects highest available version number';
+
+like_program_out 0, 'env LC_MESSAGES=C psql -h 127.0.0.1 -l', 2, qr/could not connect|connection to server .* failed/,
+ 'connecting to localhost fails with no clusters';
+
+# We check if PGCLUSTER, --cluster, and native psql options are evaluated with
+# correct priority. (This is related to the checks in t/090_multicluster.t, but
+# easier to do here because no clusters are running.)
+
+like_program_out 0, "env LC_MESSAGES=C PGCLUSTER=$MAJORS[-1]/127.0.0.2:5431 psql -l",
+ 2, qr/(could not connect|connection to server).*127.0.0.2.* port 5431/s, 'pg_wrapper uses host and port from PGCLUSTER';
+like_program_out 0, "env LC_MESSAGES=C PGCLUSTER=$MAJORS[-1]/127.0.0.2:5431 psql --cluster $MAJORS[-1]/127.0.0.3:5430 -l",
+ 2, qr/(could not connect|connection to server).*127.0.0.3.* port 5430/s, 'pg_wrapper uses --cluster from the command line';
+like_program_out 0, "env LC_MESSAGES=C PGCLUSTER=$MAJORS[-1]/127.0.0.2:5431 psql -h 127.0.0.3 -l",
+ 2, qr/(could not connect|connection to server).*127.0.0.3.* port 5432/s, 'pg_wrapper ignores PGCLUSTER with -h on the command line';
+like_program_out 0, "env LC_MESSAGES=C PGCLUSTER=$MAJORS[-1]/127.0.0.2:5431 psql --host 127.0.0.3 -l",
+ 2, qr/(could not connect|connection to server).*127.0.0.3.* port 5432/s, 'pg_wrapper ignores PGCLUSTER with --host on the command line';
+like_program_out 0, "env LC_MESSAGES=C PGCLUSTER=$MAJORS[-1]/127.0.0.2:5431 PGHOST=127.0.0.3 psql -l",
+ 2, qr/(could not connect|connection to server).*127.0.0.3.* port 5432/s, 'pg_wrapper ignores PGCLUSTER if PGHOST is set';
+
+# vim: filetype=perl
diff --git a/t/015_start_stop.t b/t/015_start_stop.t
new file mode 100644
index 0000000..a6c4ebf
--- /dev/null
+++ b/t/015_start_stop.t
@@ -0,0 +1,174 @@
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => 71 * @MAJORS;
+
+my $systemd = (-d "/run/systemd/system" and not $ENV{_SYSTEMCTL_SKIP_REDIRECT});
+note $systemd ? "We are running systemd" : "We are not running systemd";
+
+# check cluster status
+# arguments: <version> <pg_ctlcluster exit status> <systemctl exit status> <text to print>
+sub check_status {
+ my ($v, $ctlstatus, $scstatus, $text) = @_;
+ program_ok (0, "pg_ctlcluster $v main status", $ctlstatus, "cluster $v main $text");
+ if ($systemd) {
+ program_ok (0, "systemctl status postgresql\@$v-main", $scstatus, "service postgresql\@$v-main $text");
+ } else {
+ pass '';
+ }
+}
+
+sub check_major {
+ my $v = $_[0];
+ my $ctlstopped = $v >= 9.2 ? 3 : 1; # pg_ctl status "not running" changed in 9.2
+ note "Running tests for $v";
+
+ note "Cluster does not exist yet"; ###############################
+ check_status $v, 1, 3, "does not exist";
+
+ # try to start postgresql
+ if ($systemd) {
+ program_ok (0, "systemctl start postgresql");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql start");
+ }
+ check_status $v, 1, 3, "does not exist";
+
+ # try to start specific cluster
+ if ($systemd) {
+ program_ok (0, "systemctl start postgresql\@$v-main", 1);
+ } else {
+ program_ok (0, "/etc/init.d/postgresql start $v");
+ }
+ check_status $v, 1, 3, "does not exist";
+
+ note "Start/stop postgresql using system tools"; ###############################
+
+ # create cluster
+ program_ok (0, "pg_createcluster $v main");
+ check_status $v, $ctlstopped, 3, "is stopped";
+
+ # start postgresql
+ if ($systemd) {
+ program_ok (0, "systemctl start postgresql");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql start");
+ }
+ check_status $v, 0, 0, "is running";
+
+ # start postgresql again
+ if ($systemd) {
+ program_ok (0, "systemctl start postgresql");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql start");
+ }
+ check_status $v, 0, 0, "is already running";
+
+ # stop postgresql
+ if ($systemd) {
+ program_ok (0, "systemctl stop postgresql");
+ sleep 6; # FIXME: systemctl stop postgresql is not yet synchronous (#759725)
+ } else {
+ program_ok (0, "/etc/init.d/postgresql stop");
+ }
+ check_status $v, $ctlstopped, 3, "is stopped";
+
+ # stop postgresql again
+ if ($systemd) {
+ program_ok (0, "systemctl stop postgresql");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql stop");
+ }
+ check_status $v, $ctlstopped, 3, "is already stopped";
+
+ note "Start/stop specific cluster using system tools"; ###############################
+
+ # start cluster using system tools
+ if ($systemd) {
+ program_ok (0, "systemctl start postgresql\@$v-main");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql start $v");
+ }
+ check_status $v, 0, 0, "is running";
+
+ # try start cluster again
+ if ($systemd) {
+ program_ok (0, "systemctl start postgresql\@$v-main");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql start $v");
+ }
+ check_status $v, 0, 0, "is running";
+
+ # restart cluster
+ if ($systemd) {
+ program_ok (0, "systemctl restart postgresql\@$v-main");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql restart $v");
+ }
+ check_status $v, 0, 0, "is running";
+
+ # stop cluster
+ if ($systemd) {
+ program_ok (0, "systemctl stop postgresql\@$v-main");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql stop $v");
+ }
+ check_status $v, $ctlstopped, 3, "is stopped";
+
+ # try to stop cluster again
+ if ($systemd) {
+ program_ok (0, "systemctl stop postgresql\@$v-main");
+ } else {
+ program_ok (0, "/etc/init.d/postgresql stop $v");
+ }
+ check_status $v, $ctlstopped, 3, "is already stopped";
+
+ # drop cluster
+ program_ok (0, "pg_dropcluster $v main");
+ check_status $v, 1, 3, "does not exist";
+
+ note "Start/stop specific cluster using pg_*cluster"; ###############################
+
+ # try to start cluster
+ program_ok (0, "pg_ctlcluster start $v main", 1); # syntax variation: action version cluster
+ check_status $v, 1, 3, "does not exist";
+
+ # create cluster and start it
+ program_ok (0, "pg_createcluster $v main --start");
+ check_status $v, 0, 0, "is running";
+
+ # try to start cluster again
+ my $exitagain = $systemd ? 0 : 2;
+ program_ok (0, "pg_ctlcluster $v main start", $exitagain);
+ check_status $v, 0, 0, "is already running";
+
+ # restart cluster
+ program_ok (0, "pg_ctlcluster $v-main restart"); # syntax variation: version-cluster action
+ check_status $v, 0, 0, "is running";
+
+ # stop cluster
+ program_ok (0, "pg_ctlcluster $v main stop");
+ check_status $v, $ctlstopped, 3, "is stopped";
+
+ # try to stop cluster again
+ program_ok (0, "pg_ctlcluster $v main stop", 2);
+ check_status $v, $ctlstopped, 3, "is already stopped";
+
+ # start cluster
+ program_ok (0, "pg_ctlcluster start $v-main"); # syntax variation: action version-cluster
+ check_status $v, 0, 0, "is running";
+
+ # stop server, clean up, check for leftovers
+ program_ok (0, "pg_dropcluster $v main --stop");
+
+ check_clean;
+}
+
+foreach (@MAJORS) {
+ check_major $_;
+}
+
+# vim: filetype=perl
diff --git a/t/020_create_sql_remove.t b/t/020_create_sql_remove.t
new file mode 100644
index 0000000..d208b99
--- /dev/null
+++ b/t/020_create_sql_remove.t
@@ -0,0 +1,452 @@
+# We create a cluster, execute some basic SQL commands, drop it again, and
+# check that we did not leave anything behind.
+
+use strict;
+
+use File::Temp qw(tempdir);
+use POSIX qw/dup2/;
+use Time::HiRes qw/usleep/;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => 147 * @MAJORS;
+
+$ENV{_SYSTEMCTL_SKIP_REDIRECT} = 1; # FIXME: testsuite is hanging otherwise
+
+sub check_major {
+ my $v = $_[0];
+ note "Running tests for $v";
+
+ # create cluster
+ my $xlogdir = tempdir("/tmp/$v.xlog.XXXXXX", CLEANUP => 1);
+ rmdir $xlogdir; # recreated by initdb
+ if ($v > 8.2) {
+ my $start_command = $v >= 14 ? "$v *main *5432 *online" : # initdb --no-instructions in 14+
+ ($v >= 11 and not $PgCommon::rpm) ? "pg_ctlcluster" : "pg_ctl"; # CLUSTER_START_COMMAND supported in initdb 11+
+ like_program_out 'root', "pg_createcluster $v main --start -- -X $xlogdir", 0, qr/$start_command/,
+ "pg_createcluster $v main";
+ } else { # 8.2 does not have -X yet
+ like_program_out 'root', "pg_createcluster $v main --start", 0, qr/pg_ctl/,
+ "pg_createcluster $v main";
+ system "mv /var/lib/postgresql/$v/main/pg_xlog $xlogdir";
+ system "ln -s $xlogdir /var/lib/postgresql/$v/main/pg_xlog";
+ }
+
+ # check that a /var/run/postgresql/ pid file is created
+ my @contents = ('.s.PGSQL.5432', '.s.PGSQL.5432.lock', "$v-main.pid", "$v-main.pg_stat_tmp");
+ pop @contents if ($v < 8.4 or $v >= 15); # remove pg_stat_tmp
+ unless ($PgCommon::rpm and $v < 9.4) {
+ ok_dir '/var/run/postgresql/', [@contents],
+ 'Socket and pid file are in /var/run/postgresql/';
+ } else {
+ ok_dir '/var/run/postgresql/', [grep {/main/} @contents], 'Pid File is in /var/run/postgresql/';
+ }
+
+ # check that the xlog/wal symlink was created
+ my $first_xlog = $v >= 9.0 ? "000000010000000000000001" : "000000010000000000000000";
+ my @expectdir = ($first_xlog, "archive_status");
+ push @expectdir, "summaries" if ($v >= 17);
+ ok_dir $xlogdir, [@expectdir],
+ "xlog/wal directory $xlogdir was created";
+
+ # check pg_hba.conf auth methods
+ my $local_method = $v >= 9.1 ? 'peer' :
+ ($v >= 8.4 ? 'ident' :
+ 'sameuser'); # actually "ident sameuser", but the test is lazy
+ my $host_method = $v >= 14 ? 'scram-sha-256' : 'md5';
+ my (%local_methods, %host_methods);
+ open my $fh, "/etc/postgresql/$v/main/pg_hba.conf";
+ while (<$fh>) {
+ $local_methods{$1} = 1 if (/^local.*\s(\S+)/);
+ $host_methods{$1} = 1 if (/^host.*\s(\S+)/);
+ }
+ close $fh;
+ is_deeply [keys %local_methods], [$local_method], "local method in pg_hba.conf is $local_method";
+ is_deeply [keys %host_methods], [$host_method], "host method in pg_hba.conf is $host_method";
+
+ # verify that exactly one postgres master is running
+ my @pm_pids = pidof ('postgres');
+ is $#pm_pids, 0, 'Exactly one postgres master process running';
+
+ # check environment
+ my %safe_env = qw/LC_ALL 1 LC_CTYPE 1 LANG 1 PWD 1 PGLOCALEDIR 1 PGSYSCONFDIR 1 PG_GRANDPARENT_PID 1 PG_OOM_ADJUST_FILE 1 PG_OOM_ADJUST_VALUE 1 SHLVL 1 PGDATA 1 _ 1/;
+ my %env = pid_env 'postgres', $pm_pids[0];
+ foreach (keys %env) {
+ fail "postgres has unsafe environment variable $_" unless exists $safe_env{$_};
+ }
+
+ # activate external_pid_file
+ PgCommon::set_conf_value $v, 'main', 'postgresql.conf', 'external_pid_file', '';
+
+ # add variable to environment file, restart, check if it's there
+ open E, ">>/etc/postgresql/$v/main/environment" or
+ die 'could not open environment file for appending';
+ print E "PGEXTRAVAR1 = 1 # short one\nPGEXTRAVAR2='foo bar '\n\n# comment";
+ close E;
+ is_program_out 0, "pg_ctlcluster $v main restart", 0, '',
+ 'cluster restarts with new environment file';
+
+ @pm_pids = pidof ('postgres');
+ is $#pm_pids, 0, 'Exactly one postgres master process running';
+ %env = pid_env 'postgres', $pm_pids[0];
+ is $env{'PGEXTRAVAR1'}, '1', 'correct value of PGEXTRAVAR1 in environment';
+ is $env{'PGEXTRAVAR2'}, 'foo bar ', 'correct value of PGEXTRAVAR2 in environment';
+
+ # Now there should not be an external PID file any more, since we set it
+ # explicitly
+ unless ($PgCommon::rpm and ($v < 9.4 or $v >= 15)) {
+ ok_dir '/var/run/postgresql', [grep {! /pid/} @contents],
+ 'Socket and stats dir, but not PID file in /var/run/postgresql/';
+ } else {
+ ok_dir '/var/run/postgresql', ["$v-main.pg_stat_tmp"], 'Only stats dir in /var/run/postgresql/';
+ }
+
+ # verify that the correct client version is selected
+ like_program_out 'postgres', 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $v/,
+ 'pg_wrapper+createdb selects version number of cluster';
+
+ # we always want to use the latest version of "psql", though.
+ my $max_version = $ALL_MAJORS[-1];
+ if ($v < 9.2) {
+ # if version is older than 9.2 pick v14 at most
+ $max_version = (grep { $_ <= 14 } @ALL_MAJORS)[-1];
+ }
+ like_program_out 'postgres', 'psql --version', 0, qr/^psql \(PostgreSQL\) $max_version/,
+ "pg_wrapper+psql selects version $max_version";
+
+ my $default_log = "/var/log/postgresql/postgresql-$v-main.log";
+
+ # verify that the cluster is displayed
+ my $ls = `pg_lsclusters -h`;
+ $ls =~ s/\s+/ /g;
+ $ls =~ s/\s*$//;
+ is $ls, "$v main 5432 online postgres /var/lib/postgresql/$v/main $default_log",
+ 'pg_lscluster reports online cluster on port 5432';
+
+ # verify that the log file is actually used
+ ok !-z $default_log, 'log file is actually used';
+
+ # verify configuration file permissions
+ my $postgres_uid = (getpwnam 'postgres')[2];
+ my @st = stat "/etc/postgresql";
+ is $st[4], $postgres_uid, '/etc/postgresql is owned by user "postgres"';
+ my @st = stat "/etc/postgresql/$v";
+ is $st[4], $postgres_uid, 'version configuration directory file is owned by user "postgres"';
+ my @st = stat "/etc/postgresql/$v/main";
+ is $st[4], $postgres_uid, 'configuration directory file is owned by user "postgres"';
+
+ # verify data file permissions
+ my @st = stat "/var/lib/postgresql/$v";
+ is $st[4], $postgres_uid, 'version data directory file is owned by user "postgres"';
+ my @st = stat "/var/lib/postgresql/$v/main";
+ is $st[4], $postgres_uid, 'data directory file is owned by user "postgres"';
+
+ # verify log file permissions
+ my @logstat = stat $default_log;
+ is $logstat[2], 0100640, 'log file has 0640 permissions';
+ is $logstat[4], $postgres_uid, 'log file is owned by user "postgres"';
+ is $logstat[5], (getgrnam 'adm')[2], 'log file is owned by group "adm"';
+
+ # check default log file configuration; when not specifying -l with
+ # pg_createcluster, we should not have a 'log' symlink
+ ok !-e "/etc/postgresql/$v/main/log", 'no log symlink by default';
+ ok !-z $default_log, "$default_log is the default log if log symlink is missing";
+ like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/^$v\s+main.*$default_log\n$/;
+
+ # verify that log symlink works
+ is ((exec_as 'root', "pg_ctlcluster $v main stop"), 0, 'stopping cluster');
+ usleep $delay;
+ truncate "$default_log", 0; # empty log file
+ my $p = (PgCommon::cluster_data_directory $v, 'main') . '/mylog';
+ symlink $p, "/etc/postgresql/$v/main/log";
+ is ((exec_as 'root', "pg_ctlcluster $v main start"), 0,
+ 'restarting cluster with nondefault log symlink');
+ ok !-z $p, "log target is used as log file";
+ ok -z $default_log, "default log is not used";
+ like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/^$v\s+main.*$p\n$/;
+ is ((exec_as 'root', "pg_ctlcluster $v main stop"), 0, 'stopping cluster');
+ usleep $delay;
+ truncate "$default_log", 0; # empty log file
+
+ # verify that explicitly configured log file trumps log symlink
+ PgCommon::set_conf_value ($v, 'main', 'postgresql.conf',
+ ($v >= '8.3' ? 'logging_collector' : 'redirect_stderr'), 'on');
+ PgCommon::set_conf_value $v, 'main', 'postgresql.conf', 'log_filename', "$v#main.log";
+ is ((exec_as 'root', "pg_ctlcluster $v main start"), 0,
+ 'restarting cluster with explicitly configured log file');
+ ok -z $default_log, "default log is not used";
+ ok !-z $p, "log symlink target is used for startup message";
+ my $pg_log = $v >= 10 ? 'log' : 'pg_log'; # log directory in PGDATA changed in PG 10
+ my @l = glob ((PgCommon::cluster_data_directory $v, 'main') . "/$pg_log/$v#main.log*");
+ is $#l, 0, 'exactly one log file';
+ ok (-e $l[0] && ! -z $l[0], 'custom log is actually used');
+ SKIP: { skip "no logging_collector in $v", 2 if ($v < 8.3);
+ like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/^$v\s+main.*$v#main.log\n$/;
+ }
+
+ # clean up
+ PgCommon::disable_conf_value ($v, 'main', 'postgresql.conf',
+ ($v >= '8.3' ? 'logging_collector' : 'redirect_stderr'), '');
+ PgCommon::disable_conf_value $v, 'main', 'postgresql.conf', 'log_filename', '';
+ unlink "/etc/postgresql/$v/main/log";
+
+ # check that log creation does not escalate privileges
+ program_ok 'root', "pg_ctlcluster $v main stop", 0, 'stopping cluster';
+ unlink $default_log;
+ symlink "/etc/postgres-hack", $default_log;
+ program_ok 'root', "pg_ctlcluster $v main start", 1, 'starting cluster with rouge /var/log/postgresql symlink fails';
+ ok !-f "/etc/postgres-hack", "/etc/postgres-hack was not created";
+ unlink $default_log;
+ program_ok 'root', "pg_ctlcluster $v main start", 0, 'restarting cluster';
+
+ # verify that processes do not have an associated terminal
+ unlike_program_out 0, 'ps -o tty -U postgres h', 0, qr/tty|pts/,
+ 'postgres processes do not have an associated terminal';
+
+ # verify that SSL is enabled (which should work for user postgres in a
+ # default installation)
+ my $ssl = config_bool (PgCommon::get_conf_value $v, 'main', 'postgresql.conf', 'ssl');
+ my $ssl_linked = `ldd $PgCommon::binroot$v/bin/postgres | grep libssl`;
+ my ($os, $osversion) = os_release();
+ if ($PgCommon::rpm) {
+ isnt $ssl_linked, '', 'Server is linked with SSL support';
+ is $ssl, undef, 'SSL is disabled in postgresql.conf';
+ } elsif ($v <= 9.1 and (($os eq 'debian' and ($osversion eq 'unstable' or $osversion > 9)) or # stretch had 1.0 and 1.1
+ ($os eq 'ubuntu' and $osversion > 18.04))) { # bionic had 1.0 and 1.1
+ is $ssl_linked, '', 'Server is linked without SSL support (old version with only OpenSSL 1.0 support)';
+ is $ssl, undef, 'SSL is disabled in postgresql.conf';
+ } else {
+ isnt $ssl_linked, '', 'Server is linked with SSL support';
+ is $ssl, 1, 'SSL is enabled in postgresql.conf';
+ }
+
+ # Create user nobody, a database 'nobodydb' for him, check the database list
+ my $outref;
+ is ((exec_as 'nobody', 'psql -l 2>/dev/null', $outref), 2, 'psql -l fails for nobody');
+ is ((exec_as 'postgres', 'createuser nobody -D -R -S'), 0, 'createuser nobody');
+ is ((exec_as 'postgres', 'createdb -O nobody nobodydb'), 0, 'createdb nobodydb');
+ is ((exec_as 'nobody', 'psql -ltA|grep "|" | cut -f1-3 -d"|"', $outref), 0, 'psql -ltA succeeds for nobody');
+ is ($$outref, 'nobodydb|nobody|UTF8
+postgres|postgres|UTF8
+template0|postgres|UTF8
+template1|postgres|UTF8
+', 'psql -ltA output');
+
+ # Then fill nobodydb with some data.
+ is ((exec_as 'nobody', 'psql nobodydb -c "create table phone (name varchar(255) PRIMARY KEY, tel int NOT NULL)" 2>/dev/null'),
+ 0, 'SQL command: create table');
+ is ((exec_as 'nobody', 'psql nobodydb -c "insert into phone values (\'Bob\', 1)"'), 0, 'SQL command: insert into table values');
+ is ((exec_as 'nobody', 'psql nobodydb -c "insert into phone values (\'Alice\', 2)"'), 0, 'SQL command: insert into table values');
+ is ((exec_as 'nobody', 'psql nobodydb -c "insert into phone values (\'Bob\', 3)"'), 1, 'primary key violation');
+
+ # Check table contents
+ is_program_out 'nobody', 'psql -tAc "select * from phone order by name" nobodydb', 0,
+ 'Alice|2
+Bob|1
+', 'SQL command output: select -tA';
+ is_program_out 'nobody', 'psql -txc "select * from phone where name = \'Alice\'" nobodydb', 0,
+ 'name | Alice
+tel | 2
+
+', 'SQL command output: select -tx';
+ is_program_out 'nobody', 'psql -tAxc "select * from phone where name = \'Alice\'" nobodydb', 0,
+ 'name|Alice
+tel|2
+', 'SQL command output: select -tAx';
+
+ sub create_extension ($$) {
+ my ($v, $extension) = @_;
+ return "psql -qc 'CREATE EXTENSION $extension' nobodydb" if ($v >= 9.1);
+ return "createlang --cluster $v/main $extension nobodydb";
+ }
+
+ # Check PL/Perl untrusted
+ my $fn_cmd = 'CREATE FUNCTION read_file() RETURNS text AS \'open F, \\"/etc/passwd\\"; \\$buf = <F>; close F; return \\$buf;\' LANGUAGE plperl';
+ is ((exec_as 'nobody', create_extension($v, 'plperlu')), 1, 'CREATE EXTENSION plperlu fails for user nobody');
+ is_program_out 'postgres', create_extension($v, 'plperlu'), 0, '', 'CREATE EXTENSION plperlu succeeds for user postgres';
+ is ((exec_as 'nobody', "psql nobodydb -qc \"${fn_cmd}u;\""), 1, 'creating PL/PerlU function as user nobody fails');
+ is ((exec_as 'postgres', "psql nobodydb -qc \"${fn_cmd};\""), 1, 'creating unsafe PL/Perl function as user postgres fails');
+ is_program_out 'postgres', "psql nobodydb -qc \"${fn_cmd}u;\"", 0, '', 'creating PL/PerlU function as user postgres succeeds';
+ like_program_out 'nobody', 'psql nobodydb -Atc "select read_file()"',
+ 0, qr/^root:/, 'calling PL/PerlU function';
+
+ # Check PL/Perl trusted
+ my $pluser = ($v >= '8.3') ? 'nobody' : 'postgres'; # pg_pltemplate allows non-superusers to install trusted languages in 8.3+
+ is_program_out $pluser, create_extension($v, 'plperl'), 0, '', "CREATE EXTENSION plperl succeeds for user $pluser";
+ is ((exec_as 'nobody', "psql nobodydb -qc \"${fn_cmd};\""), 1, 'creating unsafe PL/Perl function as user nobody fails');
+ is_program_out 'nobody', 'psql nobodydb -qc "CREATE FUNCTION remove_vowels(text) RETURNS text AS \'\\$_[0] =~ s/[aeiou]/_/ig; return \\$_[0];\' LANGUAGE plperl;"',
+ 0, '', 'creating PL/Perl function as user nobody succeeds';
+ is_program_out 'nobody', 'psql nobodydb -Atc "select remove_vowels(\'foobArish\')"',
+ 0, "f__b_r_sh\n", 'calling PL/Perl function';
+
+ # Check PL/Python (untrusted)
+ SKIP: {
+ skip "No python2 support", 6 unless ($v <= 11 and $PgCommon::have_python2);
+ is_program_out 'postgres', create_extension($v, 'plpythonu'), 0, '', 'CREATE EXTENSION plpythonu succeeds for user postgres';
+ is_program_out 'postgres', 'psql nobodydb -qc "CREATE FUNCTION capitalize(text) RETURNS text AS \'import sys; return args[0].capitalize() + sys.version[0]\' LANGUAGE plpythonu;"',
+ 0, '', 'creating PL/Python function as user postgres succeeds';
+ is_program_out 'nobody', 'psql nobodydb -Atc "select capitalize(\'foo\')"',
+ 0, "Foo2\n", 'calling PL/Python function';
+ }
+
+ # Check PL/Python3 (untrusted)
+ if ($v >= '9.1') {
+ is_program_out 'postgres', create_extension($v, 'plpython3u'), 0, '', 'CREATE EXTENSION plpython3u succeeds for user postgres';
+ is_program_out 'postgres', 'psql nobodydb -qc "CREATE FUNCTION capitalize3(text) RETURNS text AS \'import sys; return args[0].capitalize() + sys.version[0]\' LANGUAGE plpython3u;"',
+ 0, '', 'creating PL/Python3 function as user postgres succeeds';
+ is_program_out 'nobody', 'psql nobodydb -Atc "select capitalize3(\'foo\')"',
+ 0, "Foo3\n", 'calling PL/Python function';
+ } else {
+ pass "Skipping PL/Python3 test for version $v...";
+ pass '...';
+ pass '...';
+ pass '...';
+ pass '...';
+ pass '...';
+ }
+
+ # Check PL/Tcl (trusted/untrusted)
+ is_program_out 'postgres', create_extension($v, 'pltcl'), 0, '', 'CREATE EXTENSION pltcl succeeds for user postgres';
+ is_program_out 'postgres', create_extension($v, 'pltclu'), 0, '', 'CREATE EXTENSION pltclu succeeds for user postgres';
+ is_program_out 'nobody', 'psql nobodydb -qc "CREATE FUNCTION tcl_max(integer, integer) RETURNS integer AS \'if {\\$1 > \\$2} {return \\$1}; return \\$2\' LANGUAGE pltcl STRICT;"',
+ 0, '', 'creating PL/Tcl function as user nobody succeeds';
+ is_program_out 'postgres', 'psql nobodydb -qc "CREATE FUNCTION tcl_max_u(integer, integer) RETURNS integer AS \'if {\\$1 > \\$2} {return \\$1}; return \\$2\' LANGUAGE pltclu STRICT;"',
+ 0, '', 'creating PL/TclU function as user postgres succeeds';
+ is_program_out 'nobody', 'psql nobodydb -Atc "select tcl_max(3,4)"', 0,
+ "4\n", 'calling PL/Tcl function';
+ is_program_out 'nobody', 'psql nobodydb -Atc "select tcl_max_u(5,4)"', 0,
+ "5\n", 'calling PL/TclU function';
+
+ # fake rotated logs to check that they are cleaned up properly
+ open L, ">$default_log.1" or die "could not open fake rotated log file";
+ print L "old log .1\n";
+ close L;
+ open L, ">$default_log.2" or die "could not open fake rotated log file";
+ print L "old log .2\n";
+ close L;
+ if (system "gzip -9 $default_log.2") {
+ die "could not gzip fake rotated log";
+ }
+
+ # Check that old-style pgdata symbolic link still works (p-common 0.90+
+ # does not create them any more, but they still need to work for existing
+ # installations)
+ is ((exec_as 'root', "pg_ctlcluster $v main stop"), 0, 'stopping cluster');
+ my $datadir = PgCommon::get_conf_value $v, 'main', 'postgresql.conf', 'data_directory';
+ symlink $datadir, "/etc/postgresql/$v/main/pgdata";
+
+ # data_directory should trump the pgdata symlink
+ PgCommon::set_conf_value $v, 'main', 'postgresql.conf', 'data_directory', '/nonexisting';
+ like_program_out 0, "pg_ctlcluster $v main start", 1,
+ qr/\/nonexisting is not accessible/,
+ 'cluster fails to start with invalid data_directory and valid pgdata symlink';
+
+ # if only pgdata symlink is present, it is authoritative
+ PgCommon::disable_conf_value $v, 'main', 'postgresql.conf', 'data_directory', 'disabled for test';
+ is_program_out 0, "pg_ctlcluster $v main start", 0, '',
+ 'cluster restarts with pgdata symlink';
+
+ # check properties of backend processes
+ pipe RH, WH;
+ my $psql = fork;
+ if (!$psql) {
+ close WH;
+ my @pw = getpwnam 'nobody';
+ change_ugid $pw[2], $pw[3];
+ open(STDIN, "<& RH");
+ dup2(POSIX::open('/dev/null', POSIX::O_WRONLY), 1);
+ exec 'psql', '-Xq', '-vPROMPT1=', 'nobodydb' or die "could not exec psql process: $!";
+ }
+ close RH;
+ select WH; $| = 1; # make unbuffered
+
+ open my $pidfile, "/var/lib/postgresql/$v/main/postmaster.pid";
+ my $master_pid = <$pidfile>;
+ chomp $master_pid;
+ close $pidfile;
+
+ my $client_pid;
+ while (!$client_pid) {
+ usleep $delay;
+ $client_pid = `ps --user postgres hu | grep 'postgres.*: nobody nobodydb' | grep -v grep | awk '{print \$2}'`;
+ ($client_pid) = ($client_pid =~ /(\d+)/); # untaint
+ }
+
+ # OOM score adjustment under Linux: postmaster gets bigger shields for >=
+ # 9.0, but client backends stay at default; this might not work in
+ # containers with restricted privileges, so skip the check there
+ my $adj;
+ my $detect_virt = system 'systemd-detect-virt --container --quiet'; # from systemd
+ open F, "/proc/$master_pid/oom_score_adj";
+ $adj = <F>;
+ chomp $adj;
+ close F;
+ if ($v >= '9.0' and not $PgCommon::rpm) {
+ SKIP: {
+ skip 'skipping postmaster OOM killer adjustment in container', 1 if $detect_virt == 0;
+ cmp_ok $adj, '<=', -500, 'postgres master has OOM killer protection';
+ }
+ } else {
+ is $adj, 0, 'postgres master has no OOM adjustment';
+ }
+
+ open F, "/proc/$client_pid/oom_score_adj";
+ $adj = <F>;
+ chomp $adj;
+ close F;
+ is $adj, 0, 'postgres client backend has no OOM adjustment';
+
+ # test process title update
+ like_program_out 0, "ps h $client_pid", 0, qr/ idle\s*$/, 'process title is idle';
+ print WH "BEGIN;\n";
+ usleep $delay;
+ like_program_out 0, "ps h $client_pid", 0, qr/idle in transaction/, 'process title is idle in transaction';
+
+ close WH;
+ kill 15, $psql;
+ waitpid $psql, 0;
+
+ # log file gets re-created by pg_ctlcluster
+ is ((exec_as 0, "pg_ctlcluster $v main stop"), 0, 'stopping cluster');
+ unlink $default_log;
+ is ((exec_as 0, "pg_ctlcluster $v main start"), 0, 'starting cluster as postgres works without a log file');
+ ok (-e $default_log && ! -z $default_log, 'log file got recreated and used');
+
+ # create tablespaces
+ my $spc1 = tempdir("/tmp/$v.spc1.XXXXXX", CLEANUP => 1);
+ my $spc2 = tempdir("/tmp/$v.spc2.XXXXXX", CLEANUP => 1);
+ is (mkdir ("$spc2/PG_99_fakedirectory"), 1, 'creating a directory in spc2');
+ chown $postgres_uid, 0, $spc1, $spc2, "$spc2/PG_99_fakedirectory";
+ is_program_out 'postgres', "psql -qc \"CREATE TABLESPACE spc1 LOCATION '$spc1'\"", 0, '', 'creating tablespace spc1';
+ is_program_out 'postgres', "psql -qc 'CREATE TABLE tbl1 (x int) TABLESPACE spc1'", 0, '', 'creating a table in spc1';
+ SKIP: {
+ skip "Non-empty tablespaces not supported before 9.0", 4 if ($v < 9.0);
+ is_program_out 'postgres', "psql -qc \"CREATE TABLESPACE spc2 LOCATION '$spc2'\"", 0, '', 'creating tablespace spc2';
+ is_program_out 'postgres', "psql -qc 'CREATE TABLE tbl2 (x int) TABLESPACE spc2'", 0, '', 'creating a table in spc2';
+ }
+
+ # check apt config
+ is_program_out 0, "grep -Eo 'postgresql.[0-9.*-]+' /etc/apt/apt.conf.d/02autoremove-postgresql", 0,
+ "postgresql.*-$v\n", "Correct apt NeverAutoRemove config";
+
+ # stop server, clean up, check for leftovers
+ ok ((system "pg_dropcluster $v main --stop") == 0,
+ 'pg_dropcluster removes cluster');
+
+ is (-e $xlogdir, undef, "xlog/wal directory $xlogdir was deleted");
+ ok_dir $spc1, [], "tablespace spc1 was emptied";
+ ok_dir $spc2, [qw(PG_99_fakedirectory)], "tablespace spc2 was emptied";
+
+ is_program_out 0, "grep -Eo 'postgresql.[0-9.*-]+' /etc/apt/apt.conf.d/02autoremove-postgresql", 1,
+ "", "Correct apt NeverAutoRemove config";
+
+ check_clean;
+}
+
+foreach (@MAJORS) {
+ check_major $_;
+}
+
+# vim: filetype=perl
diff --git a/t/021_pg_renamecluster.t b/t/021_pg_renamecluster.t
new file mode 100644
index 0000000..b3e1cb6
--- /dev/null
+++ b/t/021_pg_renamecluster.t
@@ -0,0 +1,43 @@
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => 20;
+
+my $v = $MAJORS[-1];
+
+# create cluster
+ok ((system "pg_createcluster $v main --start >/dev/null") == 0,
+ "pg_createcluster $v main");
+
+# test pg_renamecluster with a running cluster
+program_ok (0, "pg_renamecluster $v main donau");
+is_program_out 'postgres', 'psql -tAc "show data_directory"', 0,
+ "/var/lib/postgresql/$v/donau\n", 'cluster is running and data_directory was moved';
+is ((PgCommon::get_conf_value $v, 'donau', 'postgresql.conf', 'hba_file'),
+ "/etc/postgresql/$v/donau/pg_hba.conf", 'pg_hba.conf location updated');
+is ((PgCommon::get_conf_value $v, 'donau', 'postgresql.conf', 'ident_file'),
+ "/etc/postgresql/$v/donau/pg_ident.conf", 'pg_ident.conf location updated');
+is ((PgCommon::get_conf_value $v, 'donau', 'postgresql.conf', 'external_pid_file'),
+ "/var/run/postgresql/$v-donau.pid", 'external_pid_file location updated');
+ok (-f "/var/run/postgresql/$v-donau.pid", 'external_pid_file exists');
+SKIP: {
+ skip "no stats_temp_directory in $v", 2 if ($v < 8.4 or $v >= 15);
+ is ((PgCommon::get_conf_value $v, 'donau', 'postgresql.conf', 'stats_temp_directory'),
+ "/var/run/postgresql/$v-donau.pg_stat_tmp", 'stats_temp_directory location updated');
+ ok (-d "/var/run/postgresql/$v-donau.pg_stat_tmp", 'stats_temp_directory exists');
+}
+SKIP: {
+ skip "cluster name not supported in $v", 1 if ($v < 9.5);
+ is (PgCommon::get_conf_value ($v, 'donau', 'postgresql.conf', 'cluster_name'), "$v/donau", "cluster_name is updated");
+}
+
+# stop server, clean up, check for leftovers
+ok ((system "pg_dropcluster $v donau --stop") == 0,
+ 'pg_dropcluster removes cluster');
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/022_recovery.t b/t/022_recovery.t
new file mode 100644
index 0000000..b70a8b5
--- /dev/null
+++ b/t/022_recovery.t
@@ -0,0 +1,54 @@
+# We create a cluster, stop it ungracefully, and check if recovery works.
+
+use strict;
+
+use POSIX qw/dup2/;
+use Time::HiRes qw/usleep/;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => 17 * @MAJORS;
+
+sub check_major {
+ my $v = $_[0];
+ note "Running tests for $v";
+
+ # create cluster
+ program_ok (0, "pg_createcluster $v main --start >/dev/null");
+
+ # try an immediate shutdown and restart
+ program_ok (0, "pg_ctlcluster $v main stop -m i");
+ program_ok (0, "pg_ctlcluster $v main start");
+ my $c = 0; # fallback for when pg_isready is missing (PG < 9.3)
+ while (system ("pg_isready -q 2>&1") >> 8 == 1 and $c++ < 15) {
+ sleep(1);
+ }
+ program_ok ('postgres', "psql -c ''");
+
+ # try again with an write-protected file
+ program_ok (0, "pg_ctlcluster $v main stop -m i");
+ open F, ">/var/lib/postgresql/$v/main/foo";
+ print F "moo\n";
+ close F;
+ ok ((chmod 0444, "/var/lib/postgresql/$v/main/foo"),
+ "create write-protected file in data directory");
+ program_ok (0, "pg_ctlcluster $v main start");
+ $c = 0;
+ while (system ("pg_isready -q 2>&1") >> 8 == 1 and $c++ < 15) {
+ sleep(1);
+ }
+ program_ok ('postgres', "psql -c ''");
+
+ program_ok (0, "pg_dropcluster $v main --stop", 0,
+ 'pg_dropcluster removes cluster');
+
+ check_clean;
+}
+
+foreach (@MAJORS) {
+ check_major $_;
+}
+
+# vim: filetype=perl
diff --git a/t/025_logging.t b/t/025_logging.t
new file mode 100644
index 0000000..a7e5c50
--- /dev/null
+++ b/t/025_logging.t
@@ -0,0 +1,99 @@
+# Test various logging-related things
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Time::HiRes qw/usleep/;
+
+use Test::More tests => 55 * @MAJORS;
+
+my $syslog_works = 0;
+
+sub check_logging ($$)
+{
+ my ($text, $msg) = @_;
+ my $ls = `pg_lsclusters -h`;
+ $ls =~ s/\s+/ /g;
+ $ls =~ s/\s*$//;
+ like $ls, $text, $msg;
+}
+
+sub check_major {
+ my $v = $_[0];
+ note "Running tests for $v";
+ my $pgdata = "/var/lib/postgresql/$v/main";
+
+ # create cluster
+ ok ((system "pg_createcluster $v main --start >/dev/null") == 0,
+ "pg_createcluster $v main");
+
+ # default log setup
+ my $default_log = "/var/log/postgresql/postgresql-$v-main.log";
+ check_logging qr($v main 5432 online postgres $pgdata $default_log), "pg_lscluster reports logfile $default_log";
+ like_program_out 'postgres', "psql -qc \"'foobar_${v}_$$'\"", 1, qr/syntax error.*foobar_${v}_$$/, 'log an error';
+ usleep $delay;
+ like_program_out 'postgres', "grep --binary-files=text foobar_${v}_$$ $default_log", 0, qr/syntax error.*foobar_${v}_$$/, 'error appears in logfile';
+
+ # syslog
+ is_program_out 0, "pg_conftool $v main set log_destination syslog", 0, "", "set log_destination syslog";
+ is_program_out 0, "pg_ctlcluster $v main reload", 0, "", "$v main reload";
+ is_program_out 'postgres', "psql -Atc \"show log_destination\"", 0, "syslog\n", 'log_destination is syslog';
+ check_logging qr($v main 5432 online postgres $pgdata syslog), "pg_lscluster reports syslog";
+ SKIP: {
+ skip "/var/log/syslog not available", 2 unless ($syslog_works);
+ usleep $delay;
+ like_program_out 0, "grep --binary-files=text 'postgres.*parameter \"log_destination\" changed to \"syslog\"' /var/log/syslog", 0, qr/log_destination/, 'error appears in /var/log/syslog';
+ }
+
+ # turn logging_collector on, csvlog
+ my $pg_log = $v >= 10 ? 'log' : 'pg_log'; # log directory in PGDATA changed in PG 10
+ SKIP: {
+ skip "No logging collector in 8.2", 30 if ($v <= 8.2);
+ is_program_out 0, "pg_conftool $v main set logging_collector on", 0, "", "set logging_collector on";
+ is_program_out 0, "pg_conftool $v main set log_destination csvlog", 0, "", "set log_destination csvlog";
+ is_program_out 0, "pg_ctlcluster $v main restart", 0, "", "$v main restart";
+ is_program_out 'postgres', "psql -Atc \"show logging_collector\"", 0, "on\n", 'logging_collector is on';
+ is_program_out 'postgres', "psql -Atc \"show log_destination\"", 0, "csvlog\n", 'log_destination is csvlog';
+ check_logging qr($v main 5432 online postgres $pgdata $pg_log/.*\.csv), "pg_lscluster reports csvlog";
+ like_program_out 'postgres', "psql -qc \"'barbaz_${v}_$$'\"", 1, qr/syntax error.*barbaz_${v}_$$/, 'log an error';
+ usleep $delay;
+ like_program_out 'postgres', "grep --binary-files=text barbaz_${v}_$$ $pgdata/$pg_log/*.csv", 0, qr/syntax error.*barbaz_${v}_$$/, "error appears in $pg_log/*.csv";
+
+ # stderr,syslog,csvlog
+ is_program_out 0, "pg_conftool $v main set log_destination stderr,syslog,csvlog", 0, "", "set log_destination stderr,syslog,csvlog";
+ is_program_out 0, "pg_ctlcluster $v main reload", 0, "", "$v main reload";
+ is_program_out 'postgres', "psql -Atc \"show log_destination\"", 0, "stderr,syslog,csvlog\n", 'log_destination is stderr,syslog,csvlog';
+ check_logging qr($v main 5432 online postgres $pgdata $pg_log/.*\.log,syslog,$pg_log/.*\.csv), "pg_lscluster reports stderr,syslog,csvlog";
+ like_program_out 'postgres', "psql -qc \"'moo_${v}_$$'\"", 1, qr/syntax error.*moo_${v}_$$/, 'log an error';
+ usleep $delay;
+ like_program_out 'postgres', "grep --binary-files=text moo_${v}_$$ $pgdata/$pg_log/*.log", 0, qr/syntax error.*moo_${v}_$$/, "error appears in $pg_log/*.log";
+ SKIP: {
+ skip "/var/log/syslog not available", 2 unless ($syslog_works);
+ usleep $delay;
+ like_program_out 0, "grep --binary-files=text 'postgres.*moo_${v}_$$' /var/log/syslog", 0, qr/moo_${v}_$$/, 'error appears in /var/log/syslog';
+ }
+ like_program_out 'postgres', "grep --binary-files=text moo_${v}_$$ $pgdata/$pg_log/*.csv", 0, qr/syntax error.*moo_${v}_$$/, "error appears in $pg_log/*.csv";
+ }
+
+ # stop server, clean up, check for leftovers
+ is_program_out 0, "pg_dropcluster $v main --stop", 0, "", 'pg_dropcluster removes cluster';
+
+ check_clean;
+}
+
+system "logger -t '$0' 'test-logging-$$'";
+usleep $delay;
+if (system ("grep --binary-files=text -q 'test-logging-$$' /var/log/syslog 2> /dev/null") == 0) {
+ note 'Logging to /var/log/syslog works';
+ $syslog_works = 1;
+} else {
+ note 'Logging to /var/log/syslog does not work, skipping some syslog tests';
+}
+
+foreach (@MAJORS) {
+ check_major $_;
+}
+
+# vim: filetype=perl
diff --git a/t/030_errors.t b/t/030_errors.t
new file mode 100644
index 0000000..88d3700
--- /dev/null
+++ b/t/030_errors.t
@@ -0,0 +1,336 @@
+# Check all kinds of error conditions.
+
+use strict;
+
+require File::Temp;
+
+use lib 't';
+use TestLib;
+use Test::More tests => 156;
+use PgCommon;
+
+my $version = $MAJORS[-1];
+
+my $socketdir = '/tmp/postgresql-testsuite/';
+my ($pg_uid, $pg_gid) = (getpwnam 'postgres')[2,3];
+
+# create a pid file with content $1 and return its path
+sub create_pidfile {
+ my $fname = "/var/lib/postgresql/$version/main/postmaster.pid";
+ open F, ">$fname" or die "open: $!";
+ print F $_[0];
+ close F;
+ chown $pg_uid, $pg_gid, $fname or die "chown: $!";
+ chmod 0700, $fname or die "chmod: $!";
+ return $fname;
+}
+
+sub check_nonexisting_cluster_error {
+ my $outref;
+ my $result = exec_as 0, $_[0], $outref;
+ is $result, 1, "'$_[0]' fails";
+ like $$outref, qr/(invalid version|does not exist)/i, "$_[0] gives error message about nonexisting cluster";
+ unlike $$outref, qr/invalid symbolic link/i, "$_[0] does not print 'invalid symbolic link' gibberish";
+}
+
+# check if pg_lsclusters shows a cluster without binaries
+mkdir "/etc/postgresql/6.3";
+mkdir "/etc/postgresql/6.3/main";
+open F, ">/etc/postgresql/6.3/main/postgresql.conf";
+close F;
+is `pg_lsclusters -h`, "6.3 main <unknown> down,binaries_missing <unknown> <unknown> <unknown>\n",
+ 'pg_lscluster reports cluster without binaries';
+program_ok 0, "pg_dropcluster 6.3 main";
+
+# create cluster
+ok ((system "pg_createcluster --socketdir '$socketdir' $version main >/dev/null") == 0,
+ "pg_createcluster --socketdir");
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/$version\s*main.*5432.*down/, 'cluster was created';
+
+is ((get_cluster_port $version, 'main'), 5432, 'Port of created cluster is 5432');
+
+# creating cluster with the same name should fail
+like_program_out 'root', "pg_createcluster --socketdir '$socketdir' $version main", 1, qr/already exists/,
+ "pg_createcluster on existing cluster";
+# and the original one still exists
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/$version\s*main.*5432.*down/, 'original cluster still exists';
+
+# attempt to create clusters with an invalid port
+like_program_out 0, "pg_createcluster $version test -p foo", 1,
+ qr/invalid.*number expected/,
+ 'pg_createcluster -p checks that port option is numeric';
+like_program_out 0, "pg_createcluster $version test -p 42", 1,
+ qr/must be a positive integer between/,
+ 'pg_createcluster -p checks valid port range';
+
+# chown cluster to an invalid user to test error
+(system "chown -R 0 /var/lib/postgresql/$version/main") == 0 or die "chown failed: $!";
+like_program_out 0, "pg_ctlcluster $version main start", 1, qr/must not be owned by root/,
+ "pg_ctlcluster refuses to start root-owned cluster";
+my $badid = 98;
+(system "chown -R $badid /var/lib/postgresql/$version/main") == 0 or die "chown failed: $!";
+like_program_out 0, "pg_ctlcluster $version main start", 1, qr/owned by user id 98 which does not exist/,
+ 'pg_ctlcluster fails on invalid cluster owner uid';
+(system "chown -R postgres:$badid /var/lib/postgresql/$version/main") == 0 or die "chown failed: $!";
+like_program_out 0, "pg_ctlcluster $version main start", 1, qr/owned by group id 98 which does not exist/,
+ 'pg_ctlcluster as root fails on invalid cluster owner gid';
+like_program_out 'postgres', "pg_ctlcluster $version main start", 1, qr/owned by group id 98 which does not exist/,
+ 'pg_ctlcluster as postgres fails on invalid cluster owner gid';
+(system "chown -R postgres:postgres /var/lib/postgresql/$version/main") == 0 or die "chown failed: $!";
+program_ok 0, "pg_ctlcluster $version main start", 0,
+ 'pg_ctlcluster succeeds on valid cluster owner uid/gid';
+
+# check socket
+my @contents = ('.s.PGSQL.5432', '.s.PGSQL.5432.lock', "$version-main.pid", "$version-main.pg_stat_tmp");
+pop @contents if ($version < 8.4 or $version >= 15); # remove pg_stat_tmp
+ok_dir '/var/run/postgresql', [grep {/main/} @contents], 'No sockets in /var/run/postgresql';
+ok_dir $socketdir, ['.s.PGSQL.5432', '.s.PGSQL.5432.lock'], "Socket is in $socketdir";
+
+# stop cluster, check sockets
+ok ((system "pg_ctlcluster $version main stop") == 0,
+ 'cluster stops with custom unix_socket_dir');
+ok_dir $socketdir, [], "No sockets in $socketdir after stopping cluster";
+
+# remove default socket dir and check that the socket defaults to
+# /var/run/postgresql
+open F, "+</etc/postgresql/$version/main/postgresql.conf" or
+ die "could not open postgresql.conf for r/w: $!";
+my @lines = <F>;
+seek F, 0, 0 or die "seek: $!";
+truncate F, 0;
+@lines = grep !/^unix_socket_dir/, @lines; # <= 9.2: "_directory", >= 9.3: "_directories"
+print F @lines;
+close F;
+
+ok ((system "pg_ctlcluster $version main start") == 0,
+ 'cluster starts after removing unix_socket_dir');
+if ($PgCommon::rpm) {
+ ok ((grep { $_ eq '.s.PGSQL.5432' } @{TestLib::dircontent('/tmp')}) == 1, 'Socket is in /tmp');
+} else {
+ ok_dir '/var/run/postgresql', [@contents],
+ 'Socket is in default dir /var/run/postgresql';
+}
+ok_dir $socketdir, [], "No sockets in $socketdir";
+
+# server should not stop with corrupt file
+rename "/var/lib/postgresql/$version/main/postmaster.pid",
+ "/var/lib/postgresql/$version/main/postmaster.pid.orig" or die "rename: $!";
+create_pidfile 'foo';
+is_program_out 'postgres', "pg_ctlcluster $version main stop", 1,
+ "Error: pid file is invalid, please manually kill the stale server process.\n",
+ 'pg_ctlcluster fails with corrupted PID file';
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/online/, 'cluster is still online';
+
+# restore PID file
+(system "cp /var/lib/postgresql/$version/main/postmaster.pid.orig /var/lib/postgresql/$version/main/postmaster.pid") == 0 or die "cp: $!";
+is ((exec_as 'postgres', "pg_ctlcluster $version main stop"), 0,
+ 'pg_ctlcluster succeeds with restored PID file');
+mkdir $PgCommon::binroot . "foo"; # #940220: infinite recursion in get_program_path
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/down/, 'cluster is down';
+rmdir $PgCommon::binroot . "foo";
+
+# stop stopped server
+is_program_out 'postgres', "pg_ctlcluster $version main stop", 2,
+ "Cluster is not running.\n", 'pg_ctlcluster stop fails on stopped cluster';
+
+# simulate crashed server
+rename "/var/lib/postgresql/$version/main/postmaster.pid.orig",
+ "/var/lib/postgresql/$version/main/postmaster.pid" or die "rename: $!";
+is_program_out 'postgres', "pg_ctlcluster $version main start", 0,
+ "Removed stale pid file.\n", 'pg_ctlcluster succeeds with already existing PID file';
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/online/, 'cluster is online';
+is ((exec_as 'postgres', "pg_ctlcluster $version main stop"), 0,
+ 'pg_ctlcluster stop succeeds');
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/down/, 'cluster is down';
+ok (! -e "/var/lib/postgresql/$version/main/postmaster.pid", 'no pid file left');
+
+# trying to stop a stopped server cleans up corrupt and stale pid files
+my $pf = create_pidfile 'foo';
+is_program_out 'postgres', "pg_ctlcluster $version main stop", 2,
+ "Removed stale pid file.\nCluster is not running.\n",
+ 'pg_ctlcluster stop succeeds with corrupted PID file';
+ok (! -e $pf, 'pid file was cleaned up');
+
+create_pidfile 'foo';
+is_program_out 'postgres', "pg_ctlcluster --force $version main stop", 2,
+ "Removed stale pid file.\nCluster is not running.\n",
+ 'pg_ctlcluster --force stop succeeds with corrupted PID file';
+ok (! -e $pf, 'pid file was cleaned up');
+
+create_pidfile '99998';
+is_program_out 'postgres', "pg_ctlcluster $version main stop", 2,
+ "Removed stale pid file.\nCluster is not running.\n",
+ 'pg_ctlcluster stop succeeds with stale PID file';
+ok (! -e $pf, 'pid file was cleaned up');
+
+create_pidfile '99998';
+is_program_out 'postgres', "pg_ctlcluster --force $version main stop", 2,
+ "Removed stale pid file.\nCluster is not running.\n",
+ 'pg_ctlcluster --force stop succeeds with stale PID file';
+ok (! -e $pf, 'pid file was cleaned up');
+
+create_pidfile '';
+is_program_out 'postgres', "pg_ctlcluster --force $version main stop", 2,
+ "Removed stale pid file.\nCluster is not running.\n",
+ 'pg_ctlcluster stop succeeds with empty PID file';
+ok (! -e $pf, 'pid file was cleaned up');
+
+# corrupt PID file while server is down
+create_pidfile 'foo';
+is_program_out 'postgres', "pg_ctlcluster $version main start", 0,
+ "Removed stale pid file.\n", 'pg_ctlcluster succeeds with corrupted PID file';
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/online/, 'cluster is online';
+
+# start running server
+is_program_out 'postgres', "pg_ctlcluster $version main start", 2,
+ "Cluster is already running.\n", 'pg_ctlcluster start fails on running cluster';
+is ((exec_as 'postgres', "pg_ctlcluster $version main stop"), 0, 'pg_ctlcluster stop');
+
+# backup pg_hba.conf
+rename "/etc/postgresql/$version/main/pg_hba.conf",
+ "/etc/postgresql/$version/main/pg_hba.conf.orig" or die "rename: $!";
+
+# test check for invalid pg_hba.conf
+open F, ">/etc/postgresql/$version/main/pg_hba.conf" or die "could not create pg_hba.conf: $!";
+print F "foo\n";
+close F;
+chmod 0644, "/etc/postgresql/$version/main/pg_hba.conf" or die "chmod: $!";
+
+if ($version < '8.4') {
+ like_program_out 'postgres', "pg_ctlcluster $version main start", 0,
+ qr/WARNING.*connection to the database failed.*pg_hba.conf/is,
+ 'pg_ctlcluster start warns about invalid pg_hba.conf';
+ is_program_out 'postgres', "pg_ctlcluster $version main stop", 0, '', 'stopping cluster';
+} else {
+ like_program_out 'postgres', "pg_ctlcluster $version main start", 1,
+ qr/FATAL.*pg_hba.conf/is,
+ 'pg_ctlcluster start fails on invalid pg_hba.conf';
+ is_program_out 'postgres', "pg_ctlcluster $version main stop", 2,
+ "Cluster is not running.\n", 'stopping cluster';
+}
+
+# test check for pg_hba.conf with removed passwordless local superuser access
+open F, ">/etc/postgresql/$version/main/pg_hba.conf" or die "could not create pg_hba.conf: $!";
+print F "local all all md5\n";
+close F;
+chmod 0644, "/etc/postgresql/$version/main/pg_hba.conf" or die "chmod: $!";
+
+like_program_out 'postgres', "pg_ctlcluster $version main start", 0,
+ qr/Warning.*connection to the database failed.*(no password supplied|password authentication failed)/is,
+ 'pg_ctlcluster start warns about absence of passwordless superuser connection';
+is_program_out 'postgres', "pg_ctlcluster $version main stop", 0, '', 'stopping cluster';
+
+# restore pg_hba.conf
+unlink "/etc/postgresql/$version/main/pg_hba.conf";
+rename "/etc/postgresql/$version/main/pg_hba.conf.orig",
+ "/etc/postgresql/$version/main/pg_hba.conf" or die "rename: $!";
+
+# leftover files must not create confusion
+open F, '>/etc/postgresql/postgresql.conf';
+print F "data_directory = '/nonexisting'\n";
+close F;
+my @c = get_version_clusters $version;
+is_deeply (\@c, ['main'],
+ 'leftover /etc/postgresql/postgresql.conf is not regarded as a cluster');
+unlink '/etc/postgresql/postgresql.conf';
+
+# fails by default due to access restrictions
+# remove cluster and directory; this should work as user "postgres"
+is_program_out 'postgres', "pg_dropcluster $version main", 0, '',
+ , "pg_dropcluster works as user postgres";
+
+# graceful handling of absent data dir (might not be mounted)
+ok ((system "pg_createcluster $version main >/dev/null") == 0,
+ "pg_createcluster succeeds");
+rename "/var/lib/postgresql/$version", "/var/lib/postgresql/$version.orig" or die "rename: $!";
+my $outref;
+is ((exec_as 0, "pg_ctlcluster $version main start", $outref, 1), 1,
+ 'pg_ctlcluster fails on nonexisting /var/lib/postgresql');
+like $$outref, qr/^Error:.*\/var\/lib\/postgresql.*not accessible.*$/, 'proper error message for nonexisting /var/lib/postgresql';
+
+rename "/var/lib/postgresql/$version.orig", "/var/lib/postgresql/$version" or die "rename: $!";
+is_program_out 'postgres', "pg_ctlcluster $version main start", 0, '',
+ 'pg_ctlcluster start succeeds again with reappeared /var/lib/postgresql';
+is_program_out 'postgres', "pg_ctlcluster $version main stop", 0, '', 'stopping cluster';
+
+# pg_ctlcluster checks colliding ports
+ok ((system "pg_createcluster $version other >/dev/null") == 0,
+ "pg_createcluster other");
+set_cluster_port $version, 'other', '5432';
+is ((exec_as 'postgres', "pg_ctlcluster $version main start"), 0,
+ 'pg_ctlcluster: main cluster on conflicting port starts');
+
+# clusters can run side by side on different socket directories
+set_cluster_socketdir $version, 'other', $socketdir;
+PgCommon::set_conf_value $version, 'other', 'postgresql.conf',
+ 'listen_addresses', ''; # otherwise they will conflict on TCP socket
+is ((exec_as 'postgres', "pg_ctlcluster $version other start"), 0,
+ 'pg_ctlcluster: other cluster starts on conflicting port, but different socket dirs');
+is ((exec_as 'postgres', "pg_ctlcluster $version other stop"), 0);
+
+# ... but will give an error when running on the same port
+set_cluster_socketdir $version, 'other', ($PgCommon::rpm and $version < 9.4) ? '/tmp' : '/var/run/postgresql';
+like_program_out 'postgres', "pg_ctlcluster $version other start", 1,
+ qr/Port conflict:.*port 5432/,
+ 'pg_ctlcluster other cluster fails on conflicting port and same socket dir';
+is_program_out 'postgres', "pg_ctlcluster $version main stop", 0, '',
+ 'stopping main cluster';
+is ((exec_as 'postgres', "pg_ctlcluster $version other start"), 0,
+ 'pg_ctlcluster: other cluster on conflicting port starts after main is down');
+ok ((system "pg_dropcluster $version other --stop") == 0,
+ 'pg_dropcluster other');
+
+# clean up
+ok ((system "pg_dropcluster $version main") == 0,
+ 'pg_dropcluster');
+ok_dir $socketdir, [], 'No sockets any more';
+rmdir $socketdir or die "rmdir: $!";
+
+# ensure sane error messages for nonexisting clusters
+check_nonexisting_cluster_error 'pg_lsclusters 4.5 foo';
+check_nonexisting_cluster_error 'psql --cluster 4.5/foo';
+check_nonexisting_cluster_error "psql --cluster $MAJORS[0]/foo";
+check_nonexisting_cluster_error "pg_dropcluster 4.5 foo";
+check_nonexisting_cluster_error "pg_dropcluster $MAJORS[0] foo";
+check_nonexisting_cluster_error "pg_upgradecluster 4.5 foo";
+check_nonexisting_cluster_error "pg_upgradecluster $MAJORS[0] foo";
+check_nonexisting_cluster_error "pg_ctlcluster 4.5 foo stop";
+check_nonexisting_cluster_error "pg_ctlcluster $MAJORS[0] foo stop";
+
+check_clean;
+
+# check that pg_dropcluster copes with partially existing cluster
+# configurations (which can happen if the disk becomes full)
+
+mkdir '/etc/postgresql/';
+mkdir "/etc/postgresql/$MAJORS[-1]";
+mkdir "/etc/postgresql/$MAJORS[-1]/broken" or die "mkdir: $!";
+symlink "/var/lib/postgresql/$MAJORS[-1]/broken", "/etc/postgresql/$MAJORS[-1]/broken/pgdata" or die "symlink: $!";
+
+unlike_program_out 0, "pg_dropcluster $MAJORS[-1] broken", 0, qr/error/i,
+ 'pg_dropcluster cleans up broken cluster configuration (only /etc with pgdata)';
+
+check_clean;
+
+mkdir '/etc/postgresql/';
+mkdir '/var/lib/postgresql/';
+mkdir "/etc/postgresql/$MAJORS[-1]" and
+mkdir "/etc/postgresql/$MAJORS[-1]/broken";
+mkdir "/var/lib/postgresql/$MAJORS[-1]";
+mkdir "/var/lib/postgresql/$MAJORS[-1]/broken";
+chown $pg_uid, $pg_gid, "/var/lib/postgresql/$MAJORS[-1]/broken";
+mkdir "/var/lib/postgresql/$MAJORS[-1]/broken/base" or die "mkdir: $!";
+open F, ">/etc/postgresql/$MAJORS[-1]/broken/postgresql.conf" or die "open: $!";
+print F "data_directory = '/var/lib/postgresql/$MAJORS[-1]/broken'\n";
+close F;
+open F, ">/var/lib/postgresql/$MAJORS[-1]/broken/PG_VERSION" or die "open: $!";
+close F;
+
+unlike_program_out 0, "pg_dropcluster $MAJORS[-1] broken", 0, qr/error/i,
+ 'pg_dropcluster cleans up broken cluster configuration (/etc with pgdata and postgresql.conf and partial /var)';
+is -d "/etc/postgresql/$MAJORS[-1]", undef, "/etc/postgresql/$MAJORS[-1] was removed";
+is -d "/var/lib/postgresql/$MAJORS[-1]", undef, "/var/lib/postgresql/$MAJORS[-1] was removed";
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/031_errors_disk_full.t b/t/031_errors_disk_full.t
new file mode 100644
index 0000000..bc0a860
--- /dev/null
+++ b/t/031_errors_disk_full.t
@@ -0,0 +1,86 @@
+# Check for proper ENOSPC handling
+
+use strict;
+
+require File::Temp;
+
+use lib 't';
+use TestLib;
+use Test::More tests => $ENV{NO_TMPFS} ? 1 : 22;
+
+# skip tests if NO_TMPFS is set
+if ($ENV{NO_TMPFS}) {
+ pass 'Skipping disk full tests, NO_TMPFS is set';
+ exit;
+}
+
+# we are using unshare here, won't work with systemd
+$ENV{_SYSTEMCTL_SKIP_REDIRECT} = 1;
+
+my $outref;
+
+#
+note 'check that a failed pg_createcluster leaves no cruft behind: try creating a cluster on a 10 MB tmpfs';
+my $cmd = <<EOF;
+exec 2>&1
+set -e
+mount --make-rprivate / 2> /dev/null || :
+mkdir -p /var/lib/postgresql
+trap "umount /var/lib/postgresql" 0 HUP INT QUIT ILL ABRT PIPE TERM
+mount -t tmpfs -o size=10000000 none /var/lib/postgresql
+# this is supposed to fail
+LC_MESSAGES=C pg_createcluster $MAJORS[-1] test && exit 1 || true
+echo -n "ls>"
+# should not output anything
+ls /etc/postgresql
+ls /var/lib/postgresql
+echo "<ls"
+EOF
+
+my $result;
+$result = exec_as 'root', "echo '$cmd' | unshare -m sh", $outref;
+
+is $result, 0, 'script failed';
+like $$outref, qr/No space left on device/i,
+ 'pg_createcluster fails due to insufficient disk space';
+like $$outref, qr/\nls><ls\n/, 'does not leave files behind';
+
+check_clean;
+
+#
+note 'check disk full conditions on startup';
+my $cmd = <<EOF;
+set -e
+mount --make-rprivate / 2> /dev/null || :
+export LC_MESSAGES=C
+dirs="/etc/postgresql /var/lib/postgresql /var/log/postgresql"
+mkdir -p \$dirs
+trap "umount \$dirs" 0 HUP INT QUIT ILL ABRT PIPE TERM
+mount -t tmpfs -o size=1000000 none /etc/postgresql
+# an empty cluster needs 69MB on ppc64el, round up to 90
+mount -t tmpfs -o size=90000000 none /var/lib/postgresql
+mount -t tmpfs -o size=1000000 none /var/log/postgresql
+pg_createcluster $MAJORS[-1] test
+
+# fill up /var/lib/postgresql
+! cat < /dev/zero > /var/lib/postgresql/cruft 2>/dev/null
+echo '-- full lib --'
+! pg_ctlcluster $MAJORS[-1] test start
+echo '-- end full lib --'
+echo '-- full lib log --'
+cat /var/log/postgresql/postgresql-$MAJORS[-1]-test.log
+echo '-- end full lib log --'
+rm /var/lib/postgresql/cruft
+pg_dropcluster $MAJORS[-1] test --stop
+EOF
+
+$result = exec_as 'root', "echo '$cmd' | unshare -m sh", $outref;
+is $result, 0, 'script failed';
+like $$outref, qr/^-- full lib --.*No space left on device.*^-- end full lib --/ims,
+ 'pg_ctlcluster prints error message';
+like $$outref, qr/^-- full lib log --.*No space left on device.*^-- end full lib log --/ims,
+ 'log file has error message';
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/032_ssl_key_permissions.t b/t/032_ssl_key_permissions.t
new file mode 100644
index 0000000..929f08a
--- /dev/null
+++ b/t/032_ssl_key_permissions.t
@@ -0,0 +1,60 @@
+use strict;
+use warnings;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => $PgCommon::rpm ? 1 : 3 + 19 * @MAJORS;
+
+if ($PgCommon::rpm) { pass 'No ssl key checks on RedHat'; exit; }
+
+my ($pg_uid, $pg_gid) = (getpwnam 'postgres')[2,3];
+my $ssl_cert_gid = (getgrnam 'ssl-cert')[2]; # reset permissions
+die "Could not determine ssl-cert gid" unless ($ssl_cert_gid);
+
+my $snakekey = '/etc/ssl/private/ssl-cert-snakeoil.key';
+is ((stat $snakekey)[4], 0, "$snakekey is owned by root");
+is ((stat $snakekey)[5], $ssl_cert_gid, "$snakekey group is ssl-cert");
+is ((stat $snakekey)[2], 0100640, "$snakekey mode is 0640");
+
+foreach my $version (@MAJORS) {
+ my $pkgversion = `dpkg-query -f '\${Version}' -W postgresql-$version`;
+ note "$version ($pkgversion)";
+ if ($version <= 9.1) {
+ pass "no SSL support on $version" foreach (1..19);
+ next;
+ }
+SKIP: {
+ skip "No SSL key check on <= 9.0", 19 if ($version <= 9.0);
+ program_ok (0, "pg_createcluster $version main");
+
+ my $nobody_uid = (getpwnam 'nobody')[2];
+ chown $nobody_uid, 0, $snakekey;
+ like_program_out 'postgres', "pg_ctlcluster $version main start", 1,
+ qr/private key file.*must be owned by the database user or root/s,
+ 'ssl key owned by nobody refused';
+
+SKIP: {
+ skip "SSL key group check skipped on Debian oldstable packages", 4 if ($version <= 9.4 and $pkgversion !~ /pgdg/);
+ chown 0, 0, $snakekey;
+ chmod 0644, $snakekey;
+ like_program_out 'postgres', "pg_ctlcluster $version main start", 1,
+ qr/private key file.*has group or world access/,
+ 'ssl key with permissions root:root 0644 refused';
+
+ chown $pg_uid, $pg_gid, $snakekey;
+ chmod 0640, $snakekey;
+ like_program_out 'postgres', "pg_ctlcluster $version main start", 1,
+ qr/private key file.*has group or world access/,
+ 'ssl key with permissions postgres:postgres 0640 refused';
+}
+
+ chown 0, $ssl_cert_gid, $snakekey;
+
+ program_ok (0, "pg_dropcluster $version main --stop");
+ is ((stat $snakekey)[4], 0, "$snakekey is owned by root");
+ is ((stat $snakekey)[5], $ssl_cert_gid, "$snakekey group is ssl-cert");
+ is ((stat $snakekey)[2], 0100640, "$snakekey mode is 0640");
+ check_clean;
+}
+}
diff --git a/t/040_upgrade.t b/t/040_upgrade.t
new file mode 100644
index 0000000..6818ef8
--- /dev/null
+++ b/t/040_upgrade.t
@@ -0,0 +1,272 @@
+# Test upgrading from the oldest version to the latest, using the default
+# configuration file.
+
+# Lowest supported "upgrade from" version: 8.4 (lower versions don't have lo_import)
+# Lowest supported "upgrade to" version: 9.2 (lower versions don't have pg_upgrade -o)
+# Lowest supported "upgrade to" version with pg_dumpall: 9.1 (lower versions don't have pg_dumpall --quote-all-identifiers)
+
+use strict;
+
+use File::Temp qw/tempfile tempdir/;
+use POSIX qw/dup2/;
+use Time::HiRes qw/usleep/;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => (@MAJORS == 1) ? 1 : 123 * 3;
+
+if (@MAJORS == 1) {
+ pass 'only one major version installed, skipping upgrade tests';
+ exit 0;
+}
+
+foreach my $upgrade_options ('-m dump', '-m upgrade', '-m upgrade --link') {
+next if ($ENV{UPGRADE_METHOD} and $upgrade_options !~ /$ENV{UPGRADE_METHOD}$/); # hack to ease debugging individual methods
+note ("upgrade method \"$upgrade_options\", $MAJORS[0] -> $MAJORS[-1]");
+
+# create cluster
+ok ((system "pg_createcluster $MAJORS[0] upgr >/dev/null") == 0,
+ "pg_createcluster $MAJORS[0] upgr");
+exec_as 'root', "sed -i '/^local.*postgres/ s/\$/\\nlocal all foo trust/' /etc/postgresql/$MAJORS[0]/upgr/pg_hba.conf";
+is ((system "pg_ctlcluster $MAJORS[0] upgr start"), 0, 'Starting upgr cluster');
+
+# Create nobody user, test database, and put a table into it
+is ((exec_as 'postgres', 'createuser nobody -D -R -s && createdb -O nobody test && createdb -O nobody testnc && createdb -O nobody testro'),
+ 0, 'Create nobody user and test databases');
+is ((exec_as 'nobody', 'psql test -c "CREATE TABLE phone (name varchar(255) PRIMARY KEY, tel int NOT NULL)"'),
+ 0, 'create table');
+is ((exec_as 'nobody', 'psql test -c "INSERT INTO phone VALUES (\'Alice\', 2)"'), 0, 'insert Alice into phone table');
+SKIP: {
+ skip 'datallowconn = f not supported with pg_upgrade', 1 if $upgrade_options =~ /upgrade/;
+ is ((exec_as 'postgres', 'psql template1 -c "UPDATE pg_database SET datallowconn = \'f\' WHERE datname = \'testnc\'"'),
+ 0, 'disallow connection to testnc');
+}
+is ((exec_as 'nobody', 'psql testro -c "CREATE TABLE nums (num int NOT NULL); INSERT INTO nums VALUES (1)"'), 0, 'create table in testro');
+SKIP: {
+ skip 'read-only not supported with pg_upgrade', 2 if $upgrade_options =~ /upgrade/;
+ is ((exec_as 'postgres', 'psql template1 -c "ALTER DATABASE testro SET default_transaction_read_only TO on"'),
+ 0, 'set testro transaction default to readonly');
+ is ((exec_as 'nobody', 'psql testro -c "CREATE TABLE test(num int)"'),
+ 1, 'creating table in testro fails');
+}
+
+# create a schema and a table with a name that was un-reserved between 8.4 and 9.1
+is ((exec_as 'nobody', 'psql test -c "CREATE SCHEMA \"old\""'),
+ 0, 'create schema "old"');
+is ((exec_as 'nobody', 'psql test -c "CREATE TABLE \"old\".\"old\" (\"old\" text)"'),
+ 0, 'create table "old.old"');
+
+# create a sequence
+is ((exec_as 'nobody', 'psql test -c "CREATE SEQUENCE odd10 INCREMENT BY 2 MINVALUE 1 MAXVALUE 10 CYCLE"'),
+ 0, 'create sequence');
+is_program_out 'nobody', 'psql -Atc "SELECT nextval(\'odd10\')" test', 0, "1\n",
+ 'check next sequence value';
+is_program_out 'nobody', 'psql -Atc "SELECT nextval(\'odd10\')" test', 0, "3\n",
+ 'check next sequence value';
+
+# create a large object
+my ($fh, $filename) = tempfile("lo_import.XXXXXX", TMPDIR => 1, UNLINK => 1);
+print $fh "Hello world";
+close $fh;
+chmod 0644, $filename;
+is_program_out 'postgres', "psql -Atc \"SELECT lo_import('$filename', 1234)\"", 0, "1234\n",
+ 'create large object';
+
+# create stored procedures
+if ($MAJORS[0] < 9.0) {
+ is_program_out 'postgres', 'createlang plpgsql test', 0, '', 'createlang plpgsql test';
+} else {
+ pass '>= 9.0 enables PL/pgsql by default';
+ pass '...';
+}
+is_program_out 'nobody', 'psql test -c "CREATE FUNCTION inc2(integer) RETURNS integer LANGUAGE plpgsql AS \'BEGIN RETURN \$1 + 2; END;\';"',
+ 0, "CREATE FUNCTION\n", 'CREATE FUNCTION inc2';
+SKIP: {
+ skip 'hardcoded library paths not supported by pg_upgrade', 2 if $upgrade_options =~ /upgrade/;
+ is_program_out 'postgres', "psql -c \"UPDATE pg_proc SET probin = '$PgCommon::binroot$MAJORS[0]/lib/plpgsql.so' where proname = 'plpgsql_call_handler';\" test",
+ 0, "UPDATE 1\n", 'hardcoding plpgsql lib path';
+}
+is_program_out 'nobody', 'psql test -c "CREATE FUNCTION inc3(integer) RETURNS integer LANGUAGE plpgsql AS \'BEGIN RETURN \$1 + 3; END;\';"',
+ 0, "CREATE FUNCTION\n", 'create function inc3';
+is_program_out 'nobody', 'psql -Atc "SELECT inc2(3)" test', 0, "5\n",
+ 'call function inc2';
+is_program_out 'nobody', 'psql -Atc "SELECT inc3(3)" test', 0, "6\n",
+ 'call function inc3';
+
+# create user and group
+is_program_out 'postgres', "psql -qc 'CREATE USER foo' template1", 0, '',
+ 'create user foo';
+is_program_out 'postgres', "psql -qc 'CREATE GROUP gfoo' template1", 0, '',
+ 'create group gfoo';
+
+# create per-database and per-table ACL
+is_program_out 'postgres', "psql -qc 'GRANT CREATE ON DATABASE test TO foo'", 0, '',
+ 'GRANT CREATE ON DATABASE';
+is_program_out 'postgres', "psql -qc 'GRANT INSERT ON phone TO foo' test", 0, '',
+ 'GRANT INSERT';
+
+# exercise ACL on old database to ensure they are working
+is_program_out 'nobody', 'psql -U foo -qc "CREATE SCHEMA s_foo" test', 0, '',
+ 'CREATE SCHEMA on old cluster (ACL)';
+is_program_out 'nobody', 'psql -U foo -qc "INSERT INTO phone VALUES (\'Bob\', 1)" test',
+ 0, '', 'insert Bob into phone table (ACL)';
+
+# set config parameters
+is_program_out 'postgres', "pg_conftool $MAJORS[0] upgr set log_statement all",
+ 0, '', 'set postgresql.conf parameter';
+SKIP: {
+ skip 'postgresql.auto.conf not supported before 9.4', 6 if ($MAJORS[0] < 9.4);
+ is_program_out 'postgres', "psql -qc \"ALTER SYSTEM SET ident_file = '/etc/postgresql/$MAJORS[0]/upgr/pg_ident.conf'\"",
+ 0, '', 'set ident_file in postgresql.auto.conf';
+ is_program_out 'postgres', 'psql -qc "ALTER SYSTEM SET log_min_duration_statement = \'10s\'"',
+ 0, '', 'set log_min_duration_statement in postgresql.auto.conf';
+ is_program_out 'postgres', "echo \"data_directory = '/var/lib/postgresql/$MAJORS[0]/upgr'\" >> /var/lib/postgresql/$MAJORS[0]/upgr/postgresql.auto.conf", 0, "", "Append bogus data_directory setting to postgresql.auto.conf";
+}
+is_program_out 'postgres', 'psql -qc "ALTER DATABASE test SET DateStyle = \'ISO, YMD\'"',
+ 0, '', 'set database parameter';
+
+# create a tablespace
+my $tdir = tempdir (CLEANUP => 1);
+my ($p_uid, $p_gid) = (getpwnam 'postgres')[2,3];
+chown $p_uid, $p_gid, $tdir;
+is_program_out 'postgres', "psql -qc \"CREATE TABLESPACE myts LOCATION '$tdir'\"",
+ 0, '', "creating tablespace in $tdir";
+is_program_out 'postgres', "psql -qc 'CREATE TABLE tstab (a int) TABLESPACE myts'",
+ 0, '', "creating table in tablespace";
+
+# Check clusters
+like_program_out 'nobody', 'pg_lsclusters -h', 0,
+ qr/^$MAJORS[0]\s+upgr\s+5432 online postgres/;
+
+# Check SELECT in original cluster
+my $select_old;
+is ((exec_as 'nobody', 'psql -tAc "SELECT * FROM phone ORDER BY name" test', $select_old), 0, 'SELECT in original cluster succeeds');
+is ($$select_old, 'Alice|2
+Bob|1
+', 'check SELECT output in original cluster');
+
+# create inaccessible cwd, to check for confusing error messages
+rmdir '/tmp/pgtest';
+mkdir '/tmp/pgtest/' or die "Could not create temporary test directory /tmp/pgtest: $!";
+chmod 0100, '/tmp/pgtest/';
+chdir '/tmp/pgtest';
+
+# Upgrade to latest version
+my $outref;
+is ((exec_as 0, "(env LC_MESSAGES=C pg_upgradecluster -v $MAJORS[-1] $upgrade_options $MAJORS[0] upgr | sed -e 's/^/STDOUT: /')", $outref, 0), 0, 'pg_upgradecluster succeeds');
+like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+my @err = grep (!/^STDOUT: /, split (/\n/, $$outref));
+if (@err) {
+ fail 'no error messages during upgrade';
+ print (join ("\n", @err));
+} else {
+ pass "no error messages during upgrade";
+}
+
+# remove inaccessible test cwd
+chdir '/';
+rmdir '/tmp/pgtest/';
+
+# Check clusters
+like_program_out 'nobody', 'pg_lsclusters -h', 0,
+ qr"$MAJORS[0] +upgr 5433 down postgres /var/lib/postgresql/$MAJORS[0]/upgr +/var/log/postgresql/postgresql-$MAJORS[0]-upgr.log\n$MAJORS[-1] +upgr 5432 online postgres /var/lib/postgresql/$MAJORS[-1]/upgr +/var/log/postgresql/postgresql-$MAJORS[-1]-upgr.log", 'pg_lsclusters output';
+
+# Check that SELECT output is identical
+is_program_out 'nobody', 'psql -tAc "SELECT * FROM phone ORDER BY name" test', 0,
+ $$select_old, 'SELECT output is the same in original and upgraded test';
+is_program_out 'nobody', 'psql -tAc "SELECT * FROM nums" testro', 0,
+ "1\n", 'SELECT output is the same in original and upgraded testro';
+
+# Check that table was analyzed
+like_program_out 'nobody', "psql -XAtc \"select analyze_count from pg_stat_user_tables where relname = 'phone'\" test", 0, qr/^[1-3]$/,
+ 'check analyze count'; # --analyze-in-stages does 3 passes
+
+# Check sequence value
+is_program_out 'nobody', 'psql -Atc "SELECT nextval(\'odd10\')" test', 0, "5\n",
+ 'check next sequence value';
+is_program_out 'nobody', 'psql -Atc "SELECT nextval(\'odd10\')" test', 0, "7\n",
+ 'check next sequence value';
+is_program_out 'nobody', 'psql -Atc "SELECT nextval(\'odd10\')" test', 0, "9\n",
+ 'check next sequence value';
+is_program_out 'nobody', 'psql -Atc "SELECT nextval(\'odd10\')" test', 0, "1\n",
+ 'check next sequence value (wrap)';
+
+# check large objects
+is_program_out 'postgres', 'psql -Aqtc "SET bytea_output = \'escape\'; SELECT data FROM pg_largeobject WHERE loid = 1234"', 0, "Hello world\n",
+ 'check large object';
+
+# check stored procedures
+is_program_out 'nobody', 'psql -Atc "SELECT inc2(-3)" test', 0, "-1\n",
+ 'call function inc2';
+is_program_out 'nobody', 'psql -Atc "SELECT inc3(1)" test', 0, "4\n",
+ 'call function inc3 (formerly hardcoded path)';
+
+SKIP: {
+ skip 'upgrading databases with datallowcon = false not supported by pg_upgrade', 2 if $upgrade_options =~ /upgrade/;
+
+ # Check connection permissions
+ my $testnc_conn = $upgrade_options =~ /upgrade/ ? 't' : 'f';
+ is_program_out 'nobody', 'psql -tAc "SELECT datname, datallowconn FROM pg_database ORDER BY datname" template1', 0,
+ "postgres|t
+template0|f
+template1|t
+test|t
+testnc|$testnc_conn
+testro|t
+", 'dataallowconn values';
+}
+
+# check ACLs
+is_program_out 'nobody', 'psql -U foo -qc "CREATE SCHEMA s_bar" test', 0, '',
+ 'CREATE SCHEMA on new cluster (ACL)';
+is_program_out 'nobody', 'psql -U foo -qc "INSERT INTO phone VALUES (\'Chris\', 5)" test',
+ 0, '', 'insert Chris into phone table (ACL)';
+
+# check default transaction r/o
+is ((exec_as 'nobody', 'psql test -c "CREATE TABLE test(num int)"'),
+ 0, 'creating table in test succeeds');
+SKIP: {
+ skip 'read-only not supported by pg_upgrade', 2 if $upgrade_options =~ /upgrade/;
+ is ((exec_as 'nobody', 'psql testro -c "CREATE TABLE test(num int)"'),
+ 1, 'creating table in testro fails');
+ is ((exec_as 'postgres', 'psql testro -c "CREATE TABLE test(num int)"'),
+ 1, 'creating table in testro as superuser fails');
+}
+is ((exec_as 'nobody', 'psql testro -c "BEGIN READ WRITE; CREATE TABLE test(num int); COMMIT"'),
+ 0, 'creating table in testro succeeds with RW transaction');
+
+# check config parameters
+is_program_out 'postgres', 'psql -Atc "SHOW log_statement" test', 0, "all\n", 'check postgresql.conf parameters';
+SKIP: {
+ skip 'postgresql.auto.conf not supported before 9.4', 4 if ($MAJORS[0] < 9.4);
+ is_program_out 'postgres', 'psql -Atc "SHOW log_min_duration_statement" test', 0, "10s\n", 'check postgresql.auto.conf parameter';
+ is_program_out 'postgres', "cat /var/lib/postgresql/$MAJORS[-1]/upgr/postgresql.auto.conf", 0,
+ "# Do not edit this file manually!\n# It will be overwritten by the ALTER SYSTEM command.\nident_file = '/etc/postgresql/$MAJORS[-1]/upgr/pg_ident.conf'\nlog_min_duration_statement = '10s'\n#data_directory = '/var/lib/postgresql/$MAJORS[0]/upgr' #not valid in postgresql.auto.conf\n";
+}
+is_program_out 'postgres', 'psql -Atc "SHOW DateStyle" test', 0, "ISO, YMD\n", 'check database parameter';
+SKIP: {
+ skip "cluster name not supported in $MAJORS[0]", 1 if ($MAJORS[0] < 9.5);
+ is (PgCommon::get_conf_value ($MAJORS[-1], 'upgr', 'postgresql.conf', 'cluster_name'), "$MAJORS[-1]/upgr", "cluster_name is updated");
+}
+
+# check tablespace
+is_program_out 'postgres', "psql -Atc 'SELECT spcname FROM pg_tablespace ORDER BY spcname'",
+ 0, "myts\npg_default\npg_global\n", "check tablespace of upgraded table";
+is_program_out 'postgres', "psql -Atc \"SELECT spcname FROM pg_class c LEFT JOIN pg_tablespace t ON (c.reltablespace = t.oid) WHERE c.relname = 'tstab'\"",
+ 0, "myts\n", "check tablespace of upgraded table";
+
+# stop servers, clean up
+is ((system "pg_dropcluster $MAJORS[0] upgr --stop"), 0, 'Dropping original cluster');
+is ((system "pg_ctlcluster $MAJORS[-1] upgr restart"), 0, 'Restarting upgraded cluster');
+is_program_out 'nobody', 'psql -Atc "SELECT nextval(\'odd10\')" test', 0, "3\n",
+ 'upgraded cluster still works after removing old one';
+is ((system "pg_dropcluster $MAJORS[-1] upgr --stop"), 0, 'Dropping upgraded cluster');
+is ((system "rm -rf /var/log/postgresql/pg_upgradecluster-*"), 0, 'Cleaning pg_upgrade log files');
+
+check_clean;
+} # foreach method
+
+# vim: filetype=perl
diff --git a/t/041_upgrade_custompaths.t b/t/041_upgrade_custompaths.t
new file mode 100644
index 0000000..3416b55
--- /dev/null
+++ b/t/041_upgrade_custompaths.t
@@ -0,0 +1,51 @@
+# Test cluster upgrade with a custom data directory and custom log file.
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => (@MAJORS == 1) ? 1 : 28;
+
+if (@MAJORS == 1) {
+ pass 'only one major version installed, skipping upgrade tests';
+ exit 0;
+}
+
+ok ((system "pg_createcluster --start --datadir /tmp/postgresql-test -l /tmp/postgresql-test.log $MAJORS[0] upgr >/dev/null") == 0);
+
+# Upgrade to latest version
+my $outref;
+is ((exec_as 0, "pg_upgradecluster -v $MAJORS[-1] $MAJORS[0] upgr", $outref, 0), 0, 'pg_upgradecluster succeeds');
+like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+
+# Check clusters
+like_program_out 'nobody', 'pg_lsclusters -h', 0,
+ qr"$MAJORS[0] +upgr 5433 down postgres /tmp/postgresql-test +/tmp/postgresql-test.log\n$MAJORS[-1] +upgr 5432 online postgres /var/lib/postgresql/$MAJORS[-1]/upgr +/var/log/postgresql/postgresql-$MAJORS[-1]-upgr.log", 'pg_lsclusters output';
+
+# clean away new cluster and restart the old one
+is ((system "pg_dropcluster $MAJORS[-1] upgr --stop"), 0, 'Dropping upgraded cluster');
+is_program_out 0, "pg_ctlcluster $MAJORS[0] upgr start", 0, '', 'Restarting old cluster';
+is_program_out 'nobody', 'pg_lsclusters -h', 0,
+ "$MAJORS[0] upgr 5433 online postgres /tmp/postgresql-test /tmp/postgresql-test.log
+", 'pg_lsclusters output';
+
+# Do another upgrade with using a custom defined data directory (and in passing, test --keep-port)
+my $outref;
+is ((exec_as 0, "pg_upgradecluster --keep-port -v $MAJORS[-1] $MAJORS[0] upgr /tmp/psql-common-testsuite", $outref, 0), 0, 'pg_upgradecluster succeeds');
+unlike $$outref, qr/^pg_restore: /m, 'no pg_restore error messages during upgrade';
+unlike $$outref, qr/^[A-Z]+: /m, 'no server error messages during upgrade';
+like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+
+like_program_out 'nobody', 'pg_lsclusters -h', 0,
+ qr"$MAJORS[0] +upgr 5433 down postgres /tmp/postgresql-test +/tmp/postgresql-test.log\n$MAJORS[-1] +upgr 5432 online postgres /tmp/psql-common-testsuite +/var/log/postgresql/postgresql-$MAJORS[-1]-upgr.log", 'pg_lsclusters output';
+
+# stop servers, clean up
+is ((system "pg_dropcluster $MAJORS[0] upgr"), 0, 'Dropping original cluster');
+is ((system "pg_dropcluster $MAJORS[-1] upgr --stop"), 0, 'Dropping upgraded cluster');
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/042_upgrade_rename.t b/t/042_upgrade_rename.t
new file mode 100644
index 0000000..ef44f11
--- /dev/null
+++ b/t/042_upgrade_rename.t
@@ -0,0 +1,27 @@
+# Test in-version upgrading (usually used after catalog version bumps)
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => 15 * @MAJORS;
+
+foreach my $v (@MAJORS) {
+ SKIP: {
+ skip "pg_upgrade not supported on $v", 15 if ($v < 9.2);
+ note "PostgreSQL $v";
+
+ program_ok 0, "pg_createcluster $v main --start", 0;
+ program_ok 0, "pg_upgradecluster -m upgrade --old-bindir=$PgCommon::binroot$v/bin -v $v --rename upgr $v main", 0;
+ like_program_out 0, "pg_lsclusters -h", 0, qr/$v main 5433 down.*\n$v upgr 5432 online/;
+
+ program_ok 0, "pg_dropcluster $v main --stop", 0;
+ program_ok 0, "pg_dropcluster $v upgr --stop", 0;
+ is ((system "rm -rf /var/log/postgresql/pg_upgradecluster-$v-$v-upgr.*"), 0, 'Cleaning pg_upgrade log files');
+ check_clean;
+ }
+}
+
+# vim: filetype=perl
diff --git a/t/043_upgrade_ssl_cert.t b/t/043_upgrade_ssl_cert.t
new file mode 100644
index 0000000..d7dc79e
--- /dev/null
+++ b/t/043_upgrade_ssl_cert.t
@@ -0,0 +1,79 @@
+# Test cluster upgrade with a custom ssl certificate
+
+use strict;
+use File::Temp qw/tempdir/;
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => (@MAJORS == 1 or $PgCommon::rpm) ? 1 : 22;
+
+if (@MAJORS == 1) {
+ pass 'only one major version installed, skipping upgrade tests';
+ exit 0;
+}
+if ($PgCommon::rpm) {
+ pass 'SSL certificates not handled on RedHat';
+ exit 0;
+}
+
+
+ok ((system "pg_createcluster $MAJORS[0] upgr >/dev/null") == 0);
+
+my $tdir = tempdir (CLEANUP => 1);
+my ($p_uid, $p_gid) = (getpwnam 'postgres')[2,3];
+chown $p_uid, $p_gid, $tdir;
+
+my $tempcrt = "$tdir/ssl-cert-snakeoil.pem";
+my $oldcrt = "/var/lib/postgresql/$MAJORS[0]/upgr/server.crt";
+my $newcrt = "/var/lib/postgresql/$MAJORS[-1]/upgr/server.crt";
+
+# First upgrade
+note "upgrade test: server.crt is a symlink";
+(system "cp -p /etc/ssl/certs/ssl-cert-snakeoil.pem $tempcrt") == 0 or die "cp: $!";
+unlink $oldcrt; # remove file installed by pg_createcluster
+symlink $tempcrt, $oldcrt or die "symlink: $!";
+
+# Upgrade to latest version
+my $outref;
+is ((exec_as 0, "pg_upgradecluster --start -v $MAJORS[-1] $MAJORS[0] upgr", $outref, 0), 0, 'pg_upgradecluster succeeds');
+like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+
+if ($MAJORS[-1] >= 9.2) {
+ is ((-e $newcrt), undef, "new data directory does not contain server.crt");
+ is ((PgCommon::get_conf_value $MAJORS[-1], 'upgr', 'postgresql.conf', 'ssl_cert_file'),
+ $tempcrt, "symlink server.crt target is put into ssl_cert_file");
+} else {
+ is ((-l $newcrt), 1, "new data directory contains server.crt");
+ is ((readlink $newcrt), $tempcrt, "symlink server.crt points to correct location");
+}
+
+# Clean away new cluster
+is ((system "pg_dropcluster $MAJORS[-1] upgr --stop"), 0, 'Dropping upgraded cluster');
+unlink $oldcrt or die "unlink: $!";
+
+# Second upgrade
+note "upgrade test: server.crt is a plain file";
+(system "cp -p $tempcrt $oldcrt") == 0 or die "cp: $!";
+
+# Upgrade to latest version
+my $outref;
+is ((exec_as 0, "pg_upgradecluster --start -v $MAJORS[-1] $MAJORS[0] upgr", $outref, 0), 0, 'pg_upgradecluster succeeds');
+like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+
+is ((-f $newcrt), 1, "new data directory contains server.crt file");
+if ($MAJORS[-1] >= 9.2) {
+ is ((PgCommon::get_conf_value $MAJORS[-1], 'upgr', 'postgresql.conf', 'ssl_cert_file'),
+ $newcrt, "server.crt is put into ssl_cert_file");
+} else {
+ pass "...";
+}
+
+# Stop servers, clean up
+is ((system "pg_dropcluster $MAJORS[0] upgr"), 0, 'Dropping original cluster');
+is ((system "pg_dropcluster $MAJORS[-1] upgr --stop"), 0, 'Dropping upgraded cluster');
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/045_backup.t b/t/045_backup.t
new file mode 100644
index 0000000..1258ea3
--- /dev/null
+++ b/t/045_backup.t
@@ -0,0 +1,169 @@
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More;
+use Time::HiRes qw/usleep/;
+
+my ($pg_uid, $pg_gid) = (getpwnam 'postgres')[2,3];
+my $systemd = (-d "/run/systemd/system" and not $ENV{_SYSTEMCTL_SKIP_REDIRECT});
+note $systemd ? "We are running systemd" : "We are not running systemd";
+
+foreach my $v (@MAJORS) {
+ if ($v < 9.1) {
+ ok 1, "pg_backupcluster not supported on $v";
+ next;
+ }
+ note "PostgreSQL $v";
+
+ note "create cluster";
+ program_ok 0, "pg_createcluster --locale en_US.UTF-8 $v main --start";
+ like_program_out 0, "pg_lsclusters -h", 0, qr/$v main 5432 online/;
+ program_ok 0, "pg_conftool $v main set work_mem 11MB";
+ if ($v <= 9.6) {
+ open my $hba, ">>", "/etc/postgresql/$v/main/pg_hba.conf";
+ print $hba "local replication all peer\n";
+ close $hba;
+ program_ok 0, "pg_conftool $v main set max_wal_senders 10";
+ program_ok 0, "pg_conftool $v main set wal_level archive";
+ program_ok 0, "pg_conftool $v main set max_replication_slots 10" if ($v >= 9.4);
+ program_ok 0, "pg_conftool $v main set ssl off" if ($v <= 9.1); # cert symlinks not backed up in 9.1
+ program_ok 0, "pg_ctlcluster $v main restart";
+ }
+ my $locale_provider = $v >= 15 ? "--locale-provider libc " : "";
+ program_ok $pg_uid, "createdb -E SQL_ASCII $locale_provider-T template0 mydb";
+ program_ok $pg_uid, "psql -c 'alter database mydb set search_path=public'";
+ program_ok $pg_uid, "psql -c 'create table foo (t text)' mydb";
+ program_ok $pg_uid, "psql -c \"insert into foo values ('data from backup')\" mydb";
+ program_ok $pg_uid, "psql -c 'CREATE USER myuser'";
+ program_ok $pg_uid, "psql -c 'alter role myuser set search_path=public, myschema'";
+ program_ok $pg_uid, "createdb --locale-provider icu --icu-locale de -T template0 myicudb" if ($v >= 15);
+
+ SKIP: { # in PG 10, ARID is part of globals.sql which we try to restore before databases.sql
+ skip "alter role in database handling in PG <= 10 not supported", 1 if ($v <= 10);
+ program_ok $pg_uid, "psql -c 'alter role myuser in database mydb set search_path=public, myotherschema'";
+ }
+
+ note "create directory";
+ program_ok 0, "pg_backupcluster $v main createdirectory";
+ my $dir = "/var/backups/postgresql/$v-main";
+ my @stat = stat $dir;
+ is $stat[4], $pg_uid, "$dir owned by uid postgres";
+ is $stat[5], $pg_gid, "$dir owned by gid postgres";
+
+ my @backups = ();
+ my $dump = '';
+ SKIP: {
+ skip "dump not supported before 9.3", 1 if ($v < 9.3);
+ note "dump";
+ if ($systemd) {
+ program_ok 0, "systemctl start pg_dump\@$v-main";
+ } else {
+ program_ok 0, "pg_backupcluster $v main dump";
+ }
+ ($dump) = glob "$dir/*.dump";
+ ok -d $dump, "dump created in $dump";
+ @stat = stat $dump;
+ is $stat[4], $pg_uid, "$dump owned by uid postgres";
+ is $stat[5], $pg_gid, "$dump owned by gid postgres";
+ push @backups, $dump;
+ }
+
+ note "basebackup";
+ my $receivewal_pid;
+ if ($v >= 9.5) {
+ if ($systemd) {
+ program_ok 0, "systemctl start pg_receivewal\@$v-main";
+ } else {
+ $receivewal_pid = fork;
+ if ($receivewal_pid == 0) {
+ # suppress "not renaming "000000010000000000000003.gz.partial", segment is not complete"
+ exec "pg_backupcluster $v main receivewal 2>/dev/null";
+ }
+ }
+ program_ok $pg_uid, "psql -c 'create table poke_receivewal (t text)' mydb";
+ usleep($delay);
+ my $wal = "000000010000000000000001";
+ $wal .= ".gz" if ($v >= 10);
+ $wal .= ".partial";
+ TODO: {
+ local $TODO = "WAL test is unstable";
+ ok_dir "$dir/wal", [$wal], "$dir/wal contains $wal";
+ }
+ }
+ if ($systemd) {
+ program_ok 0, "systemctl start pg_basebackup\@$v-main";
+ } else {
+ program_ok 0, "pg_backupcluster --checkpoint=fast $v main basebackup";
+ }
+ my ($basebackup) = glob "$dir/*.backup";
+ ok -d $basebackup, "dump created in $basebackup";
+ @stat = stat $basebackup;
+ is $stat[4], $pg_uid, "$basebackup owned by uid postgres";
+ is $stat[5], $pg_gid, "$basebackup owned by gid postgres";
+ push @backups, $basebackup;
+
+ note "list";
+ like_program_out 0, "pg_backupcluster $v main list", 0, qr/$dump.*$basebackup/s;
+
+ note "more database changes";
+ program_ok $pg_uid, "psql -c \"insert into foo values ('data later deleted')\" mydb";
+ program_ok $pg_uid, "psql -c \"insert into foo values ('data from archive')\" mydb";
+ my $timestamp = `su -c "psql -XAtc 'select now()'" postgres`;
+ ok $timestamp, "retrieve recovery timestamp";
+ program_ok $pg_uid, "psql -c \"delete from foo where t = 'data later deleted'\" mydb";
+ usleep($delay);
+ if ($v >= 9.5) {
+ # since we are stopping pg_receivewal before postgresql, this implicitly tests restoring from .partial WAL files as well
+ if ($systemd) {
+ program_ok 0, "systemctl stop pg_receivewal\@$v-main";
+ } else {
+ is kill('INT', $receivewal_pid), 1, "stop receivewal";
+ }
+ }
+
+ for my $backup (@backups) {
+ note "restore $backup";
+ program_ok 0, "pg_dropcluster $v main --stop";
+ program_ok 0, "pg_restorecluster $v main $backup --start --datadir /var/lib/postgresql/$v/snowflake";
+ like_program_out 0, "pg_lsclusters -h", 0, qr/$v main 5432 online postgres .var.lib.postgresql.$v.snowflake/;
+ my $outref;
+ is exec_as($pg_uid, "psql -XAtl", $outref), 0, 'psql -XAtl';
+ like $$outref, qr/^mydb\|postgres\|SQL_ASCII\|(libc\|)?en_US.UTF-8\|en_US.UTF-8\|(\|libc\||\|\|)?$/m, "mydb locales";
+ like $$outref, qr/^myicudb\|postgres\|UTF8\|(icu\|)?en_US.UTF-8\|en_US.UTF-8\|(de\|icu\||de\|\|)?$/m, "myicudb locales" if ($v >= 15);
+ is_program_out $pg_uid, "psql -XAtc 'show work_mem'", 0, "11MB\n";
+ is_program_out $pg_uid, "psql -XAtc 'select * from foo' mydb", 0, "data from backup\n";
+ is_program_out $pg_uid, "psql -XAtc \"select analyze_count between 1 and 3 from pg_stat_user_tables where relname = 'foo'\" mydb", 0,
+ "t\n"; # --analyze-in-stages does 3 passes
+ SKIP: {
+ skip "alter role in database handling in PG <= 10 not supported", 1 if ($v <= 10);
+ like_program_out $pg_uid, "psql -XAtc '\\drds'", 0, qr/myuser\|mydb\|search_path=public, myotherschema.*
+myuser\|\|search_path=public, myschema.*
+\|mydb\|search_path=public.*\n/;
+ }
+ }
+
+ if ($v >= 9.5) {
+ note "restore $basebackup with WAL archive";
+ program_ok 0, "pg_dropcluster $v main --stop";
+ program_ok 0, "pg_restorecluster $v main $basebackup --start --archive --port 5430";
+ like_program_out 0, "pg_lsclusters -h", 0, qr/$v main 5430 online postgres .var.lib.postgresql.$v.main/;
+ is_program_out $pg_uid, "psql -XAtc 'select * from foo order by t' mydb", 0, "data from archive\ndata from backup\n";
+
+ note "restore $basebackup with PITR";
+ program_ok 0, "pg_dropcluster $v main --stop";
+ program_ok 0, "pg_restorecluster $v main $basebackup --start --pitr '$timestamp'";
+ like_program_out 0, "pg_lsclusters -h", 0, qr/$v main 5432 online postgres .var.lib.postgresql.$v.main/;
+ is_program_out $pg_uid, "psql -XAtc 'select * from foo order by t' mydb", 0, "data from archive\ndata from backup\ndata later deleted\n";
+ }
+
+ program_ok 0, "pg_dropcluster $v main --stop";
+ check_clean;
+
+} # foreach version
+
+done_testing();
+
+# vim: filetype=perl
diff --git a/t/050_encodings.t b/t/050_encodings.t
new file mode 100644
index 0000000..02dd2f7
--- /dev/null
+++ b/t/050_encodings.t
@@ -0,0 +1,117 @@
+# Test locale and encoding settings in pg_createcluster.
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => @MAJORS * 52 + 8;
+
+# create a test cluster with given locale, check the locale/encoding, and
+# remove it
+# Arguments: <version> <locale> [<encoding>]
+sub check_cluster {
+ my ($v, $locale, $enc) = @_;
+ note "Checking $v $locale";
+ my $cluster_name = $locale;
+ $cluster_name =~ s/-//g; # strip dashes so postgresql@.service likes it
+ if (defined $enc) {
+ $cluster_name .= "_$enc";
+ is ((system "LC_ALL='$locale' pg_createcluster --encoding $enc --start $v $cluster_name >/dev/null 2>&1"), 0,
+ "pg_createcluster version $v for $locale with --encoding succeeded");
+ } else {
+ is ((system "pg_createcluster --start --locale=$locale $v $cluster_name >/dev/null 2>&1"), 0,
+ "pg_createcluster version $v for $locale without --encoding succeeded");
+ }
+
+ # check encoding
+ sleep 1;
+ my $outref;
+ is ((exec_as 'postgres', "psql -Atl --cluster $v/$cluster_name", $outref, 0), 0,
+ 'psql -l succeeds');
+ my $is_unicode = 0;
+ $is_unicode = 1 if defined $enc && $enc =~ /(UNICODE|UTF-8)/;
+ $is_unicode = 1 if $locale =~ /UTF-8/;
+ if ($is_unicode) {
+ like $$outref, qr/template1.*(UNICODE|UTF8)/, 'template1 is UTF-8 encoded';
+ } else {
+ unlike $$outref, qr/template1.*(UNICODE|UTF8)/, 'template1 is not UTF-8 encoded';
+ }
+
+ # create a table and stuff some ISO-8859-5 characters into it (для)
+ is ((exec_as 'postgres', "createdb test", $outref), 0, 'creating test database');
+ is_program_out 'postgres', "printf '\324\333\357' | psql -qc \"set client_encoding='iso-8859-5';
+ create table t (x varchar); copy t from stdin\" test", 0, '',
+ 'creating table with ISO-8859-5 characters';
+ is_program_out 'postgres', "echo \"set client_encoding='utf8'; select * from t\" | psql -Atq test", 0,
+ "\320\264\320\273\321\217\n", 'correct string in UTF-8';
+ is_program_out 'postgres', "echo \"set client_encoding='iso-8859-5'; select * from t\" | psql -Atq test", 0,
+ "\324\333\357\n", 'correct string in ISO-8859-5';
+
+ # do the same test with using UTF-8 as input
+ is_program_out 'postgres', "printf '\320\264\320\273\321\217' | psql -qc \"set client_encoding='utf8';
+ delete from t; copy t from stdin\" test", 0, '',
+ 'creating table with UTF-8 characters';
+ is_program_out 'postgres', "echo \"set client_encoding='utf8'; select * from t\" | psql -Atq test", 0,
+ "\320\264\320\273\321\217\n", 'correct string in UTF-8';
+ is_program_out 'postgres', "echo \"set client_encoding='iso-8859-5'; select * from t\" | psql -Atq test", 0,
+ "\324\333\357\n", 'correct string in ISO-8859-1';
+
+ # check encoding of server error messages (breaks in locale/encoding mismatches, so skip that)
+ if (!defined $enc) {
+ # temporarily disable and accept English text, since Russian translations are disabled now
+ like_program_out 'postgres', 'psql test -c "set client_encoding = \'UTF-8\'; select sqrt(-1)"', 1,
+ qr/^[^?]*(брать|отрицательного|cannot take square root)[^?]*$/, 'Server error message has correct language and encoding';
+ }
+
+ # check that we do not run into 'ignoring unconvertible UTF-8 character'
+ # breakage on nonmatching lc_messages and client_encoding
+ PgCommon::set_conf_value $v, $cluster_name, 'postgresql.conf',
+ 'client_encoding', 'UTF-8';
+ PgCommon::set_conf_value $v, $cluster_name, 'postgresql.conf',
+ 'lc_messages', 'POSIX';
+ is_program_out 0, "pg_ctlcluster $v $cluster_name restart", 0, '',
+ 'cluster starts correctly with nonmatching lc_messages and client_encoding';
+
+ # check interception of invalidly encoded/escaped strings
+ if ($is_unicode) {
+ like_program_out 'postgres',
+ 'printf "set client_encoding=\'UTF-8\'; select \'\\310\\\\\'a\'" | psql -Atq template1',
+ 0, qr/(UNICODE|UTF8).*0x(c8.*5c|c8.*27)/,
+ 'Server rejects incorrect encoding (CVE-2006-2313)';
+ like_program_out 'postgres',
+ 'printf "set client_encoding=\'SJIS\'; select \'\\\\\\\'a\'" | psql -Atq template1',
+ 0, qr/(\\' is insecure)|(unterminated quoted string)/,
+ 'Server rejects \\\' escaping in unsafe client encoding (CVE-2006-2314)';
+ if ($v >= '9.1') {
+ like_program_out 'postgres',
+ "printf \"set client_encoding='UTF-8'; set escape_string_warning='off'; select '\\\\\\'a'\" | psql -Atq template1",
+ 0, qr/unterminated quoted string/,
+ 'Server rejects obsolete \\\' escaping in unsafe client encoding (CVE-2006-2314)';
+ } else {
+ is_program_out 'postgres',
+ "printf \"set client_encoding='UTF-8'; set escape_string_warning='off'; select '\\\\\\'a'\" | psql -Atq template1",
+ 0, "'a\n", 'Server accepts \\\' escaping in safe client encoding (CVE-2006-2314)';
+ }
+ }
+
+ # drop cluster
+ is ((system "pg_dropcluster $v $cluster_name --stop"), 0, 'Dropping cluster');
+}
+
+foreach my $v (@MAJORS) {
+ check_cluster $v, 'ru_RU';
+ check_cluster $v, 'ru_RU.UTF-8';
+
+ note "Check $v locale environment variables";
+ # check LC_* over LANG domination
+ is ((system "LANGUAGE= LC_ALL=C LANG=bo_GUS.UTF-8 pg_createcluster --start $v main >/dev/null 2>&1"), 0,
+ "pg_createcluster: LC_ALL dominates LANG");
+ like_program_out 'postgres', "psql -Atl --cluster $v/main", 0,
+ qr/template1.*ASCII/, 'template1 is ASCII encoded';
+ is ((system "pg_dropcluster $v main --stop"), 0, 'Dropping cluster');
+}
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/052_upgrade_encodings.t b/t/052_upgrade_encodings.t
new file mode 100644
index 0000000..e7a7125
--- /dev/null
+++ b/t/052_upgrade_encodings.t
@@ -0,0 +1,98 @@
+# Test default and explicit encoding on upgrades
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => (@MAJORS == 1) ? 1 : 46;
+
+if (@MAJORS == 1) {
+ pass 'only one major version installed, skipping upgrade tests';
+ exit 0;
+}
+
+my $outref;
+my $oldv = $MAJORS[0];
+my $newv = $MAJORS[-1];
+
+is ((exec_as 0, "pg_createcluster --start --locale=ru_RU $oldv main", $outref), 0,
+ "creating ru_RU $oldv cluster");
+
+is ((exec_as 'postgres', 'psql -c "create database latintest" template1', $outref), 0,
+ "creating latintest DB with LATIN encoding");
+if ($oldv <= '8.3') {
+ is ((exec_as 'postgres', 'psql -c "create database asctest encoding = \'SQL_ASCII\'" template1', $outref), 0,
+ "creating asctest DB with ASCII encoding");
+} else {
+ is ((exec_as 'postgres', 'psql -c "create database asctest template = template0 lc_collate = \'C\' lc_ctype = \'C\' encoding = \'SQL_ASCII\'" template1', $outref), 0,
+ "creating asctest DB with C locale");
+}
+if ($oldv >= 15) {
+ program_ok 'postgres', 'psql -c "create database icutest template template0 locale_provider icu icu_locale de"', 0, "creating database with ICU locale";
+} else {
+ program_ok 'postgres', 'psql -c "create database icutest"', 0, "creating placeholder icutest database";
+}
+
+is ((exec_as 'postgres', "printf 'A\\324B' | psql -c \"create table t(x varchar); copy t from stdin\" latintest", $outref),
+ 0, 'write LATIN database content to latintest');
+is ((exec_as 'postgres', "printf 'A\\324B' | psql -c \"create table t(x varchar); copy t from stdin\" asctest", $outref),
+ 0, 'write LATIN database content to asctest');
+
+is_program_out 'postgres', "echo \"select * from t\" | psql -Atq latintest",
+ 0, "A\324B\n", 'old latintest DB has correctly encoded string';
+is_program_out 'postgres', "echo \"select * from t\" | psql -Atq asctest",
+ 0, "A\324B\n", 'old asctest DB has correctly encoded string';
+
+is ((exec_as 'postgres', 'psql -Atl', $outref), 0, 'psql -Atl on old cluster');
+ok ((index $$outref, 'latintest|postgres|ISO_8859_5') >= 0, 'latintest is LATIN encoded');
+ok ((index $$outref, 'asctest|postgres|SQL_ASCII') >= 0, 'asctest is ASCII encoded');
+if ($oldv >= 15) {
+ like $$outref, qr/icutest\|postgres\|ISO_8859_5\|icu\|ru_RU\|ru_RU\|de/, 'icutest has proper icu locale';
+} else {
+ like $$outref, qr/icutest\|postgres\|ISO_8859_5/, 'icutest is LATIN encoded';
+}
+ok ((index $$outref, 'template1|postgres|ISO_8859_5') >= 0, 'template1 is LATIN encoded');
+
+# upgrade without specifying locales, should be kept
+like_program_out 0, "pg_upgradecluster -v $newv $oldv main", 0, qr/^Success. Please check/m;
+
+is ((exec_as 'postgres', "psql --cluster $newv/main -Atl", $outref), 0, 'psql -Atl on upgraded cluster');
+ok ((index $$outref, 'latintest|postgres|ISO_8859_5') >= 0, 'latintest is LATIN encoded');
+ok ((index $$outref, 'asctest|postgres|SQL_ASCII') >= 0, 'asctest is ASCII encoded');
+if ($oldv >= 15) {
+ like $$outref, qr/icutest\|postgres\|ISO_8859_5\|icu\|ru_RU\|ru_RU\|de/, 'icutest has proper icu locale';
+} else {
+ like $$outref, qr/icutest\|postgres\|ISO_8859_5/, 'icutest is LATIN encoded';
+}
+ok ((index $$outref, 'template1|postgres|ISO_8859_5') >= 0, 'template1 is LATIN encoded');
+is_program_out 'postgres', "echo \"select * from t\" | psql --cluster $newv/main -Atq latintest",
+ 0, "A\324B\n", 'new latintest DB has correctly encoded string';
+
+is ((system "pg_dropcluster --stop $newv main"), 0, 'dropping upgraded cluster');
+is ((system "pg_ctlcluster $oldv main start"), 0, 'restarting old cluster');
+
+# upgrade with explicitly specifying other locale
+like_program_out 0, "pg_upgradecluster --locale ru_RU.UTF-8 -v $newv $oldv main", 0, qr/^Success. Please check/m;
+
+is ((exec_as 'postgres', "psql --cluster $newv/main -Atl", $outref), 0, 'psql -Atl on upgraded cluster');
+if ($newv >= 11) {
+ ok ((index $$outref, 'latintest|postgres|ISO_8859_5') >= 0, 'latintest is still LATIN encoded');
+} else {
+ like $$outref, qr/latintest\|postgres\|(UTF8|UNICODE)/, 'latintest is now UTF8 encoded';
+}
+ok ((index $$outref, 'asctest|postgres|SQL_ASCII') >= 0, 'asctest is ASCII encoded');
+like $$outref, qr/template1\|postgres\|(UTF8|UNICODE)/, 'template1 is UTF8 encoded';
+is_program_out 'postgres', "echo \"select * from t\" | psql --cluster $newv/main -Atq latintest",
+ 0, ($newv >= 11 ? "A\324B\n": "AдB\n"), 'new latintest DB has correctly encoded string';
+# ASCII databases don't do automatic encoding conversion, so this remains LATIN
+is_program_out 'postgres', "echo \"select * from t\" | psql --cluster $newv/main -Atq asctest",
+ 0, "A\324B\n", 'new asctest DB has correctly encoded string';
+
+is ((system "pg_dropcluster --stop $newv main"), 0, 'dropping upgraded cluster');
+
+is ((system "pg_dropcluster $oldv main"), 0, 'dropping old cluster');
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/060_obsolete_confparams.t b/t/060_obsolete_confparams.t
new file mode 100644
index 0000000..c276431
--- /dev/null
+++ b/t/060_obsolete_confparams.t
@@ -0,0 +1,83 @@
+# Test upgrading from the oldest version to all majors with all possible
+# configuration parameters set. This checks that they are correctly
+# transitioned.
+
+use strict;
+
+use lib 't';
+use TestLib;
+
+use Test::More;
+
+if (@MAJORS == 1) {
+ pass 'only one major version installed, skipping upgrade tests';
+ done_testing();
+ exit 0;
+}
+
+$ENV{_SYSTEMCTL_SKIP_REDIRECT} = 1; # FIXME: testsuite is hanging otherwise
+
+# Test one particular upgrade (old version, new version)
+sub do_upgrade {
+ my $cur = $_[0];
+ my $new = $_[1];
+ note "Testing upgrade $cur -> $new";
+
+ # Upgrade cluster
+ like_program_out 0, "env LC_MESSAGES=C pg_upgradecluster -v $new $cur main", 0, qr/^Success. Please check/m;
+ like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/$new.*online/,
+ "New $new cluster is online";
+}
+
+# create cluster for oldest version
+is_program_out 0, "pg_createcluster $MAJORS[0] main >/dev/null", 0, "";
+
+# generate configuration file with all settings and start cluster
+is_program_out 0, "sed -i -e 's/^#\\([a-z]\\)/\\1/' /etc/postgresql/$MAJORS[0]/main/postgresql.conf",
+ 0, "", "Enabling all settings in /etc/postgresql/$MAJORS[0]/main/postgresql.conf";
+like PgCommon::get_conf_value($MAJORS[0], 'main', 'postgresql.conf', 'work_mem'), qr/MB/, "work_mem is set";
+
+# tweak invalid settings
+PgCommon::set_conf_value $MAJORS[0], 'main', 'postgresql.conf', 'log_timezone', 'UTC';
+PgCommon::set_conf_value $MAJORS[0], 'main', 'postgresql.conf', 'timezone', 'UTC';
+PgCommon::disable_conf_value $MAJORS[0], 'main', 'postgresql.conf', 'include_dir', "Disable placeholder value";
+PgCommon::disable_conf_value $MAJORS[0], 'main', 'postgresql.conf', 'include_if_exists', "Disable placeholder value";
+PgCommon::disable_conf_value $MAJORS[0], 'main', 'postgresql.conf', 'include', "Disable placeholder value";
+# older versions (<= 9.1 as of 2019-03) do not support ssl anymore
+my $postgres = PgCommon::get_program_path('postgres', $MAJORS[0]);
+my $ldd = `ldd $postgres 2>/dev/null`;
+if ($ldd and $ldd !~ /libssl/) {
+ is_program_out 0, "sed -i -e 's/^ssl/#ssl/' /etc/postgresql/$MAJORS[0]/main/postgresql.conf",
+ 0, "", "Disabling ssl settings on server that does not support SSL";
+}
+
+# start server
+is_program_out 0, "pg_ctlcluster $MAJORS[0] main start", 0, "";
+
+# Loop over all but the latest major version, testing N->N+1 upgrades
+for my $index (0 .. @MAJORS - 2) {
+ do_upgrade $MAJORS[$index], $MAJORS[$index + 1]
+}
+# remove all clusters except for the first one
+for my $index (1 .. @MAJORS - 1) {
+ is_program_out 0, "pg_dropcluster $MAJORS[$index] main --stop", 0, "", "Dropping $MAJORS[$index]/main";
+}
+
+# now test a direct upgrade from oldest to newest, to also catch parameters
+# which changed several times, like syslog -> redirect_stderr ->
+# logging_collector
+if ($#MAJORS > 1) {
+ is_program_out 0, "pg_ctlcluster $MAJORS[0] main start", 0, "";
+ do_upgrade $MAJORS[0], $MAJORS[-1];
+ is_program_out 0, "pg_dropcluster $MAJORS[-1] main --stop", 0, "", "Dropping $MAJORS[-1]/main";
+} else {
+ pass 'only two available versions, skipping tests...';
+}
+
+# remove first cluster
+is_program_out 0, "pg_dropcluster $MAJORS[0] main --stop", 0, "", "Dropping $MAJORS[0]/main";
+
+check_clean;
+done_testing();
+
+# vim: filetype=perl
diff --git a/t/070_non_postgres_clusters.t b/t/070_non_postgres_clusters.t
new file mode 100644
index 0000000..06d7f13
--- /dev/null
+++ b/t/070_non_postgres_clusters.t
@@ -0,0 +1,116 @@
+# Test successful operation of clusters which are not owned by
+# postgres. Only check the oldest and newest version.
+
+use strict;
+
+use lib 't';
+use TestLib;
+
+use Test::More tests => 40;
+
+$ENV{_SYSTEMCTL_SKIP_REDIRECT} = 1; # FIXME: testsuite is hanging otherwise
+
+my $owner = 'nobody';
+my $v = $MAJORS[0];
+
+# create cluster
+is ((system "pg_createcluster -u $owner $v main >/dev/null"), 0,
+ "pg_createcluster $v main for owner $owner");
+
+# check if start is refused when config and data owner do not match
+my $pgconf = "/etc/postgresql/$v/main/postgresql.conf";
+my ($origuid, $origgid) = (stat $pgconf)[4,5];
+chown 1, 1, $pgconf;
+like_program_out 0, "pg_ctlcluster $v main start", 1, qr/do not match/, "start refused when config and data owners mismatch";
+chown $origuid, $origgid, $pgconf;
+is ((system "pg_ctlcluster $v main start"), 0, "pg_ctlcluster succeeds with owner $owner");
+
+# Check cluster
+like_program_out $owner, 'pg_lsclusters -h', 0,
+ qr/^$v\s+main\s+5432\s+online\s+$owner/,
+ 'pg_lsclusters shows running cluster';
+
+like ((ps 'postgres'), qr/^$owner.*bin\/postgres .*\/var\/lib\/postgresql\/$v\/main/m,
+ "postgres is running as user $owner");
+
+is_program_out $owner, 'ls /tmp/.s.PGSQL.*', 0, "/tmp/.s.PGSQL.5432\n/tmp/.s.PGSQL.5432.lock\n", 'socket is in /tmp';
+
+ok_dir '/var/run/postgresql', [], '/var/run/postgresql is empty';
+
+# verify owner of configuration files
+my @st;
+my $confdir = "/etc/postgresql/$v/main";
+my ($owneruid, $ownergid) = (getpwnam $owner)[2,3];
+@st = stat $confdir;
+is $st[4], $owneruid, 'conf dir is owned by user';
+is $st[5], $ownergid, 'conf dir is owned by user\'s primary group';
+my ($ok_uid, $ok_gid) = (1, 1);
+opendir D, $confdir or die "opendir: $!";
+for my $f (readdir D) {
+ next if $f eq '.' or $f eq '..';
+ @st = stat "$confdir/$f" or die "stat: $!";
+ if ($st[4] != $owneruid) {
+ note "$f is not owned by user";
+ $ok_uid = 0;
+ }
+ if ($st[5] != $ownergid) {
+ note "$f is not owned by user's primary group";
+ $ok_gid = 0;
+ }
+}
+closedir D;
+is $ok_uid, 1, "files are owned by user";
+is $ok_gid, 1, "files are owned by user's primary group";
+
+# verify log file properties
+@st = stat "/var/log/postgresql/postgresql-$v-main.log";
+is $st[2], 0100640, 'log file has 0640 permissions';
+is $st[4], $owneruid, 'log file is owned by user';
+# the log file gid setting works on RedHat, but nobody has gid 99 there (and
+# there's not good alternative for testing)
+my $loggid = $PgCommon::rpm ? (getgrnam 'adm')[2] : $ownergid;
+is $st[5], $loggid, 'log file is owned by user\'s primary group';
+
+if ($#MAJORS > 0) {
+ my $newv = $MAJORS[-1];
+
+ my $outref;
+ is ((exec_as 0, "(pg_upgradecluster -v $newv $v main | sed -e 's/^/STDOUT: /')", $outref, 0), 0,
+ 'pg_upgradecluster succeeds');
+ like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+ like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+ my @err = grep (!/^STDOUT: /, split (/\n/, $$outref));
+ if (@err) {
+ fail 'no error messages during upgrade';
+ print (join ("\n", @err));
+ } else {
+ pass "no error messages during upgrade";
+ }
+
+ # verify file permissions
+ @st = stat "/etc/postgresql/$newv/main";
+ is $st[4], $owneruid, 'upgraded conf dir is owned by user';
+ is $st[5], $ownergid, 'upgraded conf dir is owned by user\'s primary group';
+ @st = stat "/etc/postgresql/$newv/main/postgresql.conf";
+ is $st[4], $owneruid, 'upgraded postgresql.conf dir is owned by user';
+ is $st[5], $ownergid, 'upgraded postgresql.conf dir is owned by user\'s primary group';
+ @st = stat "/var/log/postgresql/postgresql-$v-main.log";
+ is $st[4], $owneruid, 'upgraded log file is owned by user';
+ is $st[5], $loggid, 'upgraded log file is owned by user\'s primary group';
+
+ is ((system "pg_dropcluster $newv main --stop"), 0, 'pg_dropcluster');
+} else {
+ pass 'only one major version installed, skipping upgrade test';
+ for (my $i = 0; $i < 10; ++$i) {
+ pass '...';
+ }
+}
+
+# Check proper cleanup
+is ((system "pg_dropcluster $v main --stop"), 0, 'pg_dropcluster');
+is_program_out $owner, 'pg_lsclusters -h', 0, '', 'No clusters left';
+is ((ps 'postgres'), '', "No postgres processes left");
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/080_start.conf.t b/t/080_start.conf.t
new file mode 100644
index 0000000..2e4d4cf
--- /dev/null
+++ b/t/080_start.conf.t
@@ -0,0 +1,145 @@
+# Check start.conf handling.
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => 73;
+
+my $systemd = -d '/run/systemd/system';
+
+# Do test with oldest version
+my $v = $MAJORS[0];
+
+# create cluster
+is ((system "pg_createcluster $v main >/dev/null"), 0, "pg_createcluster $v main");
+
+# Check that we start with 'auto'
+note "start.conf auto";
+is ((get_cluster_start_conf $v, 'main'), 'auto',
+ 'get_cluster_start_conf returns auto');
+is_program_out 'nobody', "grep '^[^\\s#]' /etc/postgresql/$v/main/start.conf",
+ 0, "auto\n", 'start.conf contains auto';
+SKIP: {
+ skip 'not running under systemd', 2 unless ($systemd);
+ ok_dir '/run/systemd/generator/postgresql.service.wants',
+ ["postgresql\@$v-main.service"],
+ "systemd generator links cluster";
+ is ((readlink "/run/systemd/generator/postgresql.service.wants/postgresql\@$v-main.service"),
+ "/lib/systemd/system/postgresql@.service",
+ "systemd generator links correct service file");
+}
+
+# init script should handle auto cluster
+like_program_out 0, "/etc/init.d/postgresql start $v", 0, qr/Start.*($v|systemctl)/;
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/online/, 'cluster is online';
+like_program_out 0, "/etc/init.d/postgresql stop $v", 0, qr/Stop.*($v|systemctl)/;
+sleep 3 if ($systemd); # FIXME: systemctl stop postgresql is not yet synchronous (#759725)
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/down/, 'cluster is down';
+
+# change to manual, verify start.conf contents
+note "start.conf manual";
+set_cluster_start_conf $v, 'main', 'manual';
+
+is ((get_cluster_start_conf $v, 'main'), 'manual',
+ 'get_cluster_start_conf returns manual');
+is_program_out 'nobody', "grep '^[^\\s#]' /etc/postgresql/$v/main/start.conf",
+ 0, "manual\n", 'start.conf contains manual';
+SKIP: {
+ skip 'not running under systemd', 1 unless ($systemd);
+ system "systemctl daemon-reload";
+ ok_dir '/run/systemd/generator/postgresql.service.wants',
+ [], "systemd generator doesn't link cluster";
+}
+
+# init script should not handle manual cluster ...
+like_program_out 0, "/etc/init.d/postgresql start $v", 0, qr/Start.*($v|systemctl)/;
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/down/, 'cluster is down';
+
+# pg_ctlcluster should handle manual cluster
+is_program_out 'postgres', "pg_ctlcluster $v main start", 0, '';
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/online/, 'cluster is online';
+is_program_out 'postgres', "pg_ctlcluster $v main stop", 0, '';
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/down/, 'cluster is down';
+
+# change to disabled, verify start.conf contents
+note "start.conf disabled";
+set_cluster_start_conf $v, 'main', 'disabled';
+
+is ((get_cluster_start_conf $v, 'main'), 'disabled',
+ 'get_cluster_start_conf returns disabled');
+SKIP: {
+ skip 'not running under systemd', 1 unless ($systemd);
+ system "systemctl daemon-reload";
+ ok_dir '/run/systemd/generator/postgresql.service.wants',
+ [], "systemd generator doesn't link cluster";
+}
+
+# init script should not handle disabled cluster
+like_program_out 0, "/etc/init.d/postgresql start $v", 0, qr/Start.*($v|systemctl)/;
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/down/, 'cluster is down';
+
+# pg_ctlcluster should not start disabled cluster
+is_program_out 'postgres', "pg_ctlcluster $v main start", 1,
+ "Error: Cluster is disabled\n";
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/down/, 'cluster is down';
+
+# change back to manual, start cluster
+set_cluster_start_conf $v, 'main', 'manual';
+is_program_out 'postgres', "pg_ctlcluster $v main start", 0, '';
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/online/, 'cluster is online';
+
+# however, we want to stop disabled clusters
+set_cluster_start_conf $v, 'main', 'disabled';
+is_program_out 'postgres', "pg_ctlcluster $v main stop", 0, '';
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/down/, 'cluster is down';
+
+# set back to manual
+set_cluster_start_conf $v, 'main', 'manual';
+is_program_out 'postgres', "pg_ctlcluster $v main start", 0, '';
+like_program_out 'postgres', 'pg_lsclusters -h', 0, qr/online/, 'cluster is online';
+
+# upgrade cluster
+note "test upgrade";
+if ($#MAJORS == 0) {
+ pass 'only one major version installed, skipping upgrade test';
+ pass '...';
+} else {
+ like_program_out 0, "pg_upgradecluster -v $MAJORS[-1] $v main", 0, qr/Success. Please check/;
+}
+
+# check start.conf of old and upgraded cluster
+is ((get_cluster_start_conf $v, 'main'), 'manual',
+ 'get_cluster_start_conf for old cluster returns manual');
+is ((get_cluster_start_conf $MAJORS[-1], 'main'), 'manual',
+ 'get_cluster_start_conf for new cluster returns manual');
+
+# clean up
+if ($#MAJORS == 0) {
+ pass '...';
+} else {
+ is ((system "pg_dropcluster $v main"), 0,
+ 'dropping old cluster');
+}
+
+is ((system "pg_dropcluster $MAJORS[-1] main --stop"), 0,
+ 'dropping upgraded cluster');
+
+is_program_out 'postgres', 'pg_lsclusters -h', 0, '', 'no clusters any more';
+
+# create cluster with --start-conf option
+is_program_out 0, "pg_createcluster $v main --start-conf foo", 1,
+ "Error: Invalid --start-conf value: foo\n",
+ 'pg_createcluster checks --start-conf validity';
+is ((system "pg_createcluster $v main --start-conf manual >/dev/null"), 0,
+ 'pg_createcluster checks --start-conf manual');
+is ((get_cluster_start_conf $v, 'main'), 'manual',
+ 'get_cluster_start_conf returns manual');
+is ((system "pg_dropcluster $v main"), 0,
+ 'dropping cluster');
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/085_pg_ctl.conf.t b/t/085_pg_ctl.conf.t
new file mode 100644
index 0000000..dca2700
--- /dev/null
+++ b/t/085_pg_ctl.conf.t
@@ -0,0 +1,51 @@
+# Check pg_ctl.conf handling.
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => $MAJORS[-1] >= '8.3' ? 33 : 1;
+
+# Do test with newest version
+my $v = $MAJORS[-1];
+if ($v < '8.3') {
+ pass 'Skipping core limit tests for versions < 8.3';
+ exit 0;
+}
+
+# enable core dumps
+# sudo and salsa-ci set the hard limit to 0 by default, undo that
+is_program_out 0, "prlimit --core=0:unlimited --pid=$$", 0, '', "set core file size to unlimited";
+is_program_out 'postgres', "sh -c 'ulimit -Hc'", 0, "unlimited\n", "core file size is unlimited";
+
+# create cluster
+is ((system "pg_createcluster $v main >/dev/null"), 0, "pg_createcluster $v main");
+ok (-f "/etc/postgresql/$v/main/pg_ctl.conf", "/etc/postgresql/$v/main/pg_ctl.conf exists");
+
+# Default behaviour, core size=0
+is_program_out 0, "pg_ctlcluster $v main start", 0, '', "starting cluster as root";
+is_program_out 'postgres', "xargs -i awk '/core/ {print \$5}' /proc/{}/limits < /var/run/postgresql/$v-main.pid", 0, "0\n", "soft core size is 0";
+my $hard_limit = `xargs -i awk '/core/ {print \$6}' /proc/{}/limits < /var/run/postgresql/$v-main.pid`;
+chomp $hard_limit;
+note "hard core file size limit of root-started postgres process is $hard_limit";
+
+# -c in pg_ctl.conf, core size=unlimited
+ok (set_cluster_pg_ctl_conf($v, 'main', '-c'), "set pg_ctl default option to -c");
+is_program_out 0, "pg_ctlcluster $v main restart", 0, '', "restarting cluster as root";
+is_program_out 'postgres', "xargs -i awk '/core/ {print \$5}' /proc/{}/limits < /var/run/postgresql/$v-main.pid", 0, "$hard_limit\n", "soft core size is $hard_limit";
+
+# Back to default behaviour, core size=0
+is_program_out 0, "pg_ctlcluster $v main stop", 0, '', "stopping cluster";
+ok (set_cluster_pg_ctl_conf($v, 'main', ''), "restored pg_ctl default option");
+
+# pg_ctl -c, core size=unlimited
+is_program_out 'postgres', "pg_ctlcluster $v main start -- -c", 0, '', "starting cluster with -c on the command line as postgres";
+is_program_out 'postgres', "xargs -i awk '/core/ {print \$5}' /proc/{}/limits < /var/run/postgresql/$v-main.pid", 0, "unlimited\n", "soft core size is unlimited";
+is_program_out 'postgres', "pg_ctlcluster $v main stop", 0, '', "stopping cluster";
+
+is ((system "pg_dropcluster $v main --stop"), 0, 'dropping cluster');
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/090_multicluster.t b/t/090_multicluster.t
new file mode 100644
index 0000000..8322ace
--- /dev/null
+++ b/t/090_multicluster.t
@@ -0,0 +1,286 @@
+# Check operation with multiple clusters
+
+use strict;
+
+use lib 't';
+use TestLib;
+use Socket;
+use PgCommon;
+
+use Test::More tests => 125;
+
+# create fake socket at 5433 to verify that this port is skipped
+socket (SOCK, PF_INET, SOCK_STREAM, getprotobyname('tcp')) or die "socket: $!";
+setsockopt(SOCK, Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1) or die "setsockopt: $!";
+bind (SOCK, sockaddr_in(5433, INADDR_ANY)) || die "bind: $! ";
+listen (SOCK, 0) or die "listen: $!";
+
+# create clusters
+is ((system "pg_createcluster $MAJORS[0] old -- -A trust >/dev/null"), 0, "pg_createcluster $MAJORS[0] old");
+is ((system "pg_createcluster $MAJORS[-1] new1 -- -A trust >/dev/null"), 0, "pg_createcluster $MAJORS[-1] new1");
+is ((system "pg_createcluster $MAJORS[-1] new2 -p 5440 -- -A trust >/dev/null"), 0, "pg_createcluster $MAJORS[-1] new2");
+close SOCK;
+
+my $old = "$MAJORS[0]/old";
+my $new1 = "$MAJORS[-1]/new1";
+my $new2 = "$MAJORS[-1]/new2";
+
+is ((system "pg_ctlcluster $MAJORS[0] old start >/dev/null"), 0, "starting cluster $old");
+is ((system "pg_ctlcluster $MAJORS[-1] new1 start >/dev/null"), 0, "starting cluster $new1");
+is ((system "pg_ctlcluster $MAJORS[-1] new2 start >/dev/null"), 0, "starting cluster $new2");
+
+like_program_out 'postgres', 'pg_lsclusters -h | sort -k3', 0, qr/.*5432.*5434.*5440.*/s,
+ 'clusters have the correct ports, skipping used 5433';
+
+# move user_clusters aside for the test; this will ensure that client programs
+# work correctly without any file at all
+if (-f '/etc/postgresql-common/user_clusters') {
+ ok ((rename '/etc/postgresql-common/user_clusters',
+ '/etc/postgresql-common/user_clusters.psqltestsuite'),
+ 'Temporarily moving away /etc/postgresql-common/user_clusters');
+} else {
+ pass '/etc/postgresql-common/user_clusters does not exist';
+}
+
+# check basic cluster selection
+like_program_out 0, 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[0]/,
+ 'pg_wrapper selects port 5432 as default cluster';
+like_program_out 0, "createdb --cluster $new1 --version", 0,
+ qr/^createdb \(PostgreSQL\) $MAJORS[-1]/,
+ 'pg_wrapper --cluster works';
+like_program_out 0, "createdb --cluster $MAJORS[-1]/foo --version", 1,
+ qr/Cluster .* does not exist/,
+ 'pg_wrapper --cluster errors out for invalid cluster';
+
+# create a database in new1 and check that it doesn't appear in new2
+is_program_out 'postgres', "createdb --cluster $new1 test", 0, ($MAJORS[-1] < 8.3 ? "CREATE DATABASE\n" : '');
+like_program_out 'postgres', "psql -Atl --cluster $new1", 0,
+ qr/test\|postgres\|/,
+ 'test db appears in cluster new1';
+unlike_program_out 'postgres', "psql -Atl --cluster $new2", 0,
+ qr/test\|postgres\|/,
+ 'test db does not appear in cluster new2';
+unlike_program_out 'postgres', "psql -Atl", 0, qr/test\|postgres\|/,
+ 'test db does not appear in default cluster';
+
+# check network cluster selection
+is_program_out 'postgres', "psql --cluster $MAJORS[0]/127.0.0.1: -Atc 'show port' template1", 0, "5432\n",
+ "psql --cluster $MAJORS[0]/127.0.0.1: defaults to port 5432";
+like_program_out 'postgres', "createdb --cluster $MAJORS[-1]/127.0.0.1:5432 --version", 0,
+ qr/^createdb \(PostgreSQL\) $MAJORS[-1]/,
+ "createdb --cluster $MAJORS[-1]/127.0.0.1:5432 uses latest client version";
+like_program_out 'postgres', "psql -Atl --cluster $MAJORS[-1]/localhost:5434", 0,
+ qr/test\|postgres\|/, "test db appears in cluster $MAJORS[-1]/localhost:5434";
+unlike_program_out 'postgres', "psql -Atl --cluster $MAJORS[-1]/localhost:5440", 0,
+ qr/test\|postgres\|/, "test db does not appear in cluster $MAJORS[-1]/localhost:5440";
+
+# check some erroneous cluster specifications
+like_program_out 'postgres', "LC_MESSAGES=C psql -Atl --cluster $MAJORS[-1]/localhost:5435", 2,
+ qr/could not connect|connection to server .* failed/, "psql --cluster $MAJORS[-1]/localhost:5435 fails due to nonexisting port";
+like_program_out 'postgres', "LC_MESSAGES=C psql -Atl --cluster $MAJORS[-1]/localhost:a", 1,
+ qr/Cluster .* does not exist/, "psql --cluster $MAJORS[-1]/localhost:a fails due to invalid syntax";
+like_program_out 'postgres', "LC_MESSAGES=C psql -Atl --cluster $MAJORS[-1]/doesnotexi.st", 1,
+ qr/Cluster .* does not exist/, "psql --cluster $MAJORS[-1]/doesnotexi.st fails due to invalid syntax";
+like_program_out 'postgres', "psql -Atl --cluster 6.4/localhost:", 1,
+ qr/Invalid version/, "psql --cluster 6.4/localhost: fails due to invalid version";
+
+# check that environment variables work
+$ENV{'PGCLUSTER'} = $new1;
+like_program_out 'postgres', "psql -Atl", 0, qr/test\|postgres\|/,
+ 'PGCLUSTER selection (1)';
+$ENV{'PGCLUSTER'} = $new2;
+unlike_program_out 'postgres', "psql -Atl", 0, qr/test\|postgres\|/,
+ 'PGCLUSTER selection (2)';
+$ENV{'PGCLUSTER'} = 'foo';
+like_program_out 'postgres', "psql -l", 1,
+ qr/Invalid version .* specified in PGCLUSTER/,
+ 'invalid PGCLUSTER value';
+$ENV{'PGCLUSTER'} = "$MAJORS[-1]/127.0.0.1:";
+like_program_out 0, 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[-1]/,
+ 'PGCLUSTER network cluster selection (1)';
+$ENV{'PGCLUSTER'} = "$MAJORS[-1]/localhost:5434";
+like_program_out 'postgres', 'psql -Atl', 0,
+ qr/test\|postgres\|/, 'PGCLUSTER network cluster selection (2)';
+$ENV{'PGCLUSTER'} = "$MAJORS[-1]/localhost:5440";
+unlike_program_out 'postgres', 'psql -Atl', 0,
+ qr/test\|postgres\|/, 'PGCLUSTER network cluster selection (3)';
+$ENV{'PGCLUSTER'} = "$MAJORS[-1]/localhost:5435";
+like_program_out 'postgres', 'LC_MESSAGES=C psql -Atl', 2,
+ qr/could not connect|connection to server .* failed/, "psql --cluster $MAJORS[-1]/localhost:5435 fails due to nonexisting port";
+delete $ENV{'PGCLUSTER'};
+
+# check that PGPORT works
+$ENV{'PGPORT'} = '5434';
+is_program_out 'postgres', 'psql -Atc "show port" template1', 0, "5434\n",
+ 'PGPORT selection (1)';
+$ENV{'PGPORT'} = '5432';
+is_program_out 'postgres', 'psql -Atc "show port" template1', 0, "5432\n",
+ 'PGPORT selection (2)';
+$ENV{'PGCLUSTER'} = $new2;
+delete $ENV{'PGPORT'};
+$ENV{'PGPORT'} = '5432';
+like_program_out 'postgres', 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[-1]/,
+ 'PGPORT+PGCLUSTER, PGCLUSTER selects version';
+is_program_out 'postgres', 'psql -Atc "show port" template1', 0, "5432\n",
+ 'PGPORT+PGCLUSTER, PGPORT selects port';
+delete $ENV{'PGPORT'};
+delete $ENV{'PGCLUSTER'};
+
+# check that PGDATABASE works
+$ENV{'PGDATABASE'} = 'test';
+is_program_out 'postgres', "psql --cluster $new1 -Atc 'select current_database()'", 0, "test\n",
+ 'PGDATABASE environment variable works';
+delete $ENV{'PGDATABASE'};
+
+# check cluster selection with an empty user_clusters
+open F, '>/etc/postgresql-common/user_clusters' or die "Could not create user_clusters: $!";
+close F;
+chmod 0644, '/etc/postgresql-common/user_clusters';
+like_program_out 0, 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[0]/,
+ 'pg_wrapper selects port 5432 as default cluster with empty user_clusters';
+like_program_out 0, "createdb --cluster $new1 --version", 0,
+ qr/^createdb \(PostgreSQL\) $MAJORS[-1]/,
+ 'pg_wrapper --cluster works with empty user_clusters';
+
+# check default cluster selection with user_clusters
+open F, '>/etc/postgresql-common/user_clusters' or die "Could not create user_clusters: $!";
+print F "* * $MAJORS[-1] new1 *\n";
+close F;
+chmod 0644, '/etc/postgresql-common/user_clusters';
+like_program_out 'postgres', 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[-1]/,
+ "pg_wrapper selects correct cluster with user_clusters '* * $MAJORS[-1] new1 *'";
+
+# check default database selection with user_clusters
+open F, '>/etc/postgresql-common/user_clusters' or die "Could not create user_clusters: $!";
+print F "* * $MAJORS[-1] new1 test\n";
+close F;
+chmod 0644, '/etc/postgresql-common/user_clusters';
+is_program_out 'postgres', 'psql -Atc "select current_database()"', 0, "test\n",
+ "pg_wrapper selects correct database with user_clusters '* * $MAJORS[-1] new1 test'";
+$ENV{'PGDATABASE'} = 'template1';
+is_program_out 'postgres', "psql -Atc 'select current_database()'", 0, "template1\n",
+ 'PGDATABASE environment variable is not overridden by user_clusters';
+delete $ENV{'PGDATABASE'};
+
+# check by-user cluster selection with user_clusters
+# (also check invalid cluster reporting)
+open F, '>/etc/postgresql-common/user_clusters' or die "Could not create user_clusters: $!";
+print F "postgres * $MAJORS[-1] new1 *\nnobody * $MAJORS[0] old *\n* * 5.5 * *";
+close F;
+chmod 0644, '/etc/postgresql-common/user_clusters';
+like_program_out 'postgres', 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[-1]/,
+ 'pg_wrapper selects correct cluster with per-user user_clusters';
+like_program_out 'nobody', 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[0]/,
+ 'pg_wrapper selects correct cluster with per-user user_clusters';
+like_program_out 0, 'createdb --version', 0, qr/user_clusters.*line 3.*version.*not exist/i,
+ 'pg_wrapper warning for invalid per-user user_clusters line';
+
+# check by-user network cluster selection with user_clusters
+# (also check invalid cluster reporting)
+open F, '>/etc/postgresql-common/user_clusters' or die "Could not create user_clusters: $!";
+print F "postgres * $MAJORS[0] localhost: *\nnobody * $MAJORS[-1] new1 *\n* * $MAJORS[-1] localhost:a *";
+close F;
+chmod 0644, '/etc/postgresql-common/user_clusters';
+like_program_out 'postgres', 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[0]/,
+ 'pg_wrapper selects correct version with per-user user_clusters';
+like_program_out 'nobody', 'createdb --version', 0, qr/^createdb \(PostgreSQL\) $MAJORS[-1]/,
+ 'pg_wrapper selects correct version with per-user user_clusters';
+like_program_out 0, 'createdb --version', 0, qr/user_clusters.*line 3.*cluster.*not exist/i,
+ 'pg_wrapper warning for invalid per-user user_clusters line';
+# check PGHOST environment variable precedence
+$ENV{'PGHOST'} = '127.0.0.2';
+like_program_out 'postgres', 'psql -Atl', 2, qr/127.0.0.2/, '$PGHOST overrides user_clusters';
+is_program_out 'postgres', "psql --cluster $MAJORS[-1]/localhost:5434 -Atc 'select current_database()' test",
+ 0, "test\n", '--cluster overrides $PGHOST';
+delete $ENV{'PGHOST'};
+
+# check invalid user_clusters
+open F, '>/etc/postgresql-common/user_clusters' or die "Could not create user_clusters: $!";
+print F 'foo';
+close F;
+chmod 0644, '/etc/postgresql-common/user_clusters';
+like_program_out 'postgres', 'createdb --version', 0, qr/ignoring invalid line 1/,
+ 'pg_wrapper ignores invalid lines in user_clusters';
+
+# remove test user_clusters
+unlink '/etc/postgresql-common/user_clusters' or die
+ "unlink user_clusters: $!";
+
+# check that pg_service.conf works
+open F, '>/etc/postgresql-common/pg_service.conf' or die "Could not create pg_service.conf: $!";
+print F "[old_t1]
+user=postgres
+dbname=template1
+port=5432
+
+[new1_test]
+user=postgres
+dbname=test
+port=5434
+
+# these do not exist
+[new2_test]
+user=postgres
+dbname=test
+port=5440
+";
+close F;
+chmod 0644, '/etc/postgresql-common/pg_service.conf';
+
+$ENV{'PGSERVICE'} = 'old_t1';
+is_program_out 'postgres', "psql -Atc 'select current_database()'", 0,
+ "template1\n", 'pg_service conf selection 1';
+$ENV{'PGSERVICE'} = 'new1_test';
+is_program_out 'postgres', "psql -Atc 'select current_database()'", 0,
+ "test\n", 'pg_service conf selection 2';
+$ENV{'PGSERVICE'} = 'new2_test';
+like_program_out 'postgres', "psql -Atc 'select current_database()'", 2,
+ qr/FATAL.*test/, 'pg_service conf selection 3';
+delete $ENV{'PGSERVICE'};
+unlink '/etc/postgresql-common/pg_service.conf';
+
+# check proper error message if no cluster could be determined as default for
+# pg_wrapper
+is ((system "pg_ctlcluster $MAJORS[0] old stop >/dev/null"), 0, "stopping cluster $old");
+PgCommon::set_conf_value $MAJORS[0], 'old', 'postgresql.conf', 'port', '5435';
+is ((system "pg_ctlcluster $MAJORS[0] old start >/dev/null"), 0, "restarting cluster $old");
+like_program_out 'postgres', 'pg_lsclusters -h | sort -k3', 0, qr/.*5434.*5435.*5440.*/s,
+ 'port of first cluster was successfully changed';
+like_program_out 'postgres', "psql -l", 2,
+ qr/no.*default.*man pg_wrapper.*psql:.*\.s\.PGSQL.5432/is,
+ 'proper pg_wrapper warning and psql error if no cluster is suitable as default target';
+like_program_out 'postgres', "psql -Atl --cluster $new1", 0,
+ qr/test\|postgres\|/,
+ '--cluster selects appropriate cluster';
+like_program_out 'postgres', "psql -Atl -p 5434", 0,
+ qr/test\|postgres\|/,
+ '-p selects appropriate cluster';
+like_program_out 'postgres', "psql -Atlp 5434", 0,
+ qr/test\|postgres\|/,
+ '-Atlp selects appropriate cluster';
+like_program_out 'postgres', "psql -Atl --port 5434", 0,
+ qr/test\|postgres\|/,
+ '--port selects appropriate cluster';
+like_program_out 'postgres', "env PGPORT=5434 psql -Atl", 0,
+ qr/test\|postgres\|/,
+ '$PGPORT selects appropriate cluster';
+
+# but specifying -p explicitly should work
+
+# restore original user_clusters
+if (-f '/etc/postgresql-common/user_clusters.psqltestsuite') {
+ ok ((rename '/etc/postgresql-common/user_clusters.psqltestsuite',
+ '/etc/postgresql-common/user_clusters'),
+ 'Restoring original /etc/postgresql-common/user_clusters');
+} else {
+ pass '/etc/postgresql-common/user_clusters did not exist, not restoring';
+}
+
+# clean up
+is ((system "pg_dropcluster $MAJORS[-1] new1 --stop"), 0, "dropping $new1");
+is ((system "pg_dropcluster $MAJORS[-1] new2 --stop"), 0, "dropping $new2");
+is ((system "pg_dropcluster $MAJORS[0] old --stop"), 0, "dropping $old");
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/110_integrate_cluster.t b/t/110_integrate_cluster.t
new file mode 100644
index 0000000..4201a58
--- /dev/null
+++ b/t/110_integrate_cluster.t
@@ -0,0 +1,44 @@
+# Check integration of an already existing cluster
+
+use strict;
+
+use lib 't';
+use TestLib;
+use File::Temp qw/tempdir/;
+use Time::HiRes qw(usleep);
+
+my $version = $MAJORS[-1];
+
+use Test::More tests => 32;
+use PgCommon;
+
+delete $ENV{'LANG'};
+delete $ENV{'LANGUAGE'};
+$ENV{'LC_ALL'} = 'C';
+
+my $wdir = tempdir (CLEANUP => 1);
+chmod 0755, $wdir or die "Could not chmod $wdir: $!";
+
+# create clusters for different owners and check their integration
+for my $o ('postgres', 'nobody') {
+ my $cdir = "$wdir/c";
+ mkdir $cdir;
+ my $oid = getpwnam $o;
+ chown $oid, 0, $cdir or die "Could not chown $cdir to $oid: $!";
+ like_program_out $o, "$PgCommon::binroot$version/bin/initdb $cdir/$o",
+ 0, qr/Success/, "creating raw initdb cluster for user $o";
+ like_program_out 0, "pg_createcluster $version $o -d $cdir/$o", 0,
+ qr/Configuring already existing cluster/i, "integrating $o cluster";
+ like_program_out 0, "pg_lsclusters", 0,
+ qr/$version\s+$o\s+5432\s+down\s+$o\s/, 'correct pg_lsclusters output';
+ is_program_out $o, "pg_ctlcluster $version $o start", 0, '', "starting cluster $o";
+ like_program_out 0, "pg_lsclusters", 0,
+ qr/$version\s+$o\s+5432\s+online\s+$o\s/, 'correct pg_lsclusters output';
+ is ((system "pg_dropcluster $version $o --stop"), 0, "dropping cluster $o");
+ ok_dir $cdir, [], 'No files in temporary cluster dir left behind';
+ rmdir $cdir;
+}
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/120_pg_upgradecluster_scripts.t b/t/120_pg_upgradecluster_scripts.t
new file mode 100644
index 0000000..d31e52c
--- /dev/null
+++ b/t/120_pg_upgradecluster_scripts.t
@@ -0,0 +1,114 @@
+# Check /etc/p-c/pg_upgradecluster.d/ scripts and proper handling of already
+# existing tables in the target cluster.
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => (@MAJORS == 1) ? 1 : 31;
+
+if (@MAJORS == 1) {
+ pass 'only one major version installed, skipping upgrade tests';
+ exit 0;
+}
+
+
+# create old cluster
+is ((system "pg_createcluster $MAJORS[0] main --start >/dev/null"), 0, "pg_createcluster $MAJORS[0] main");
+
+# add data table, auxtable with 'old...' values, and an unrelated auxtable in
+# another schema
+is_program_out 'postgres',
+ 'psql template1 -qc "create table auxdata (x varchar(10)); insert into auxdata values (\'old1\'); insert into auxdata values (\'old2\')"',
+ 0, '', 'adding auxdata to template1 and fill in some "old..." values';
+is_program_out 'postgres', "createdb test", 0, '';
+is_program_out 'postgres', 'psql test -qc "create table userdata(x int); insert into userdata values(42); insert into userdata values(256)"',
+ 0, '', 'creating userdata table';
+is_program_out 'postgres',
+ 'psql test -qc "create schema s; create table s.auxdata (x varchar(10)); insert into s.auxdata values (\'schema1\')"',
+ 0, '', 'adding schema s and s.auxdata to test and fill in some values';
+
+if (not -d '/etc/postgresql-common/pg_upgradecluster.d') {
+ mkdir '/etc/postgresql-common/pg_upgradecluster.d' or die "mkdir: $!";
+}
+
+# move existing files away
+for my $f (glob("/etc/postgresql-common/pg_upgradecluster.d/*")) {
+ next if ($f =~ /\.disabled$/);
+ rename $f, "$f.disabled";
+}
+
+# create test scripts
+chmod 0755, '/etc/postgresql-common/pg_upgradecluster.d' or die "chmod: $!";
+open F, '>/etc/postgresql-common/pg_upgradecluster.d/auxdata' or die "open: $!";
+print F <<EOS;
+#!/bin/sh -e
+# Arguments: <old version> <cluster name> <new version> <phase>
+oldver=\$1
+cluster=\$2
+newver=\$3
+phase=\$4
+
+if [ \$phase = init ]; then
+ createdb --cluster \$newver/\$cluster idb
+fi
+
+if [ \$phase = finish ]; then
+ psql --cluster \$newver/\$cluster template1 <<EOF
+drop table if exists auxdata;
+create table auxdata (x varchar(10));
+insert into auxdata values ('new1');
+insert into auxdata values ('new2');
+EOF
+fi
+
+EOS
+close F;
+chmod 0755, '/etc/postgresql-common/pg_upgradecluster.d/auxdata' or die "chmod: $!";
+
+open F, '>/etc/postgresql-common/pg_upgradecluster.d/badscript' or die "open: $!";
+print F <<EOS;
+#!/bin/false
+EOS
+close F;
+chmod 0755, '/etc/postgresql-common/pg_upgradecluster.d/badscript' or die "chmod: $!";
+
+# upgrade cluster
+my $outref;
+is ((exec_as 0, "pg_upgradecluster -v $MAJORS[-1] $MAJORS[0] main", $outref, 1), 1, 'pg_upgradecluster fails with bad script');
+like $$outref, qr/error|fail/i, 'server error messages during upgrade';
+unlink '/etc/postgresql-common/pg_upgradecluster.d/badscript';
+
+is ((exec_as 0, "pg_upgradecluster -v $MAJORS[-1] $MAJORS[0] main", $outref, 0), 0, 'pg_upgradecluster succeeds');
+unlike $$outref, qr/error|fail/i, 'no server error messages during upgrade';
+like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+
+is ((system "pg_dropcluster $MAJORS[0] main --stop"), 0, 'Dropping old cluster');
+
+# check new version cluster
+is_program_out 'postgres', 'psql template1 -Atc "select * from auxdata order by x"', 0,
+ "new1\nnew2\n", 'new cluster\'s template1/auxdata table is the script\'s version';
+
+like_program_out 'postgres', 'psql -Atl', 0, qr/^idb\b.*^test\b/ms,
+ 'upgraded cluster has idb and test databases';
+
+is_program_out 'postgres', 'psql test -Atc "select * from s.auxdata"', 0,
+ "schema1\n", 'new cluster\'s test/auxdata table in schema s was upgraded normally';
+
+# remove test script
+unlink '/etc/postgresql-common/pg_upgradecluster.d/auxdata' or die "unlink: $!";
+
+# restore original contents
+for my $f (glob("/etc/postgresql-common/pg_upgradecluster.d/*.disabled")) {
+ my $f2 = $f;
+ $f2 =~ s/\.disabled$//;
+ rename $f, $f2;
+}
+
+# clean up
+is ((system "pg_dropcluster $MAJORS[-1] main --stop"), 0, "pg_dropcluster $MAJORS[-1] main");
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/130_nonroot_admin.t b/t/130_nonroot_admin.t
new file mode 100644
index 0000000..5375b73
--- /dev/null
+++ b/t/130_nonroot_admin.t
@@ -0,0 +1,50 @@
+# Check that cluster administration works as non-root if the invoker has
+# sufficient permissions on directories.
+
+use strict;
+
+use lib 't';
+use TestLib;
+
+my $version = $MAJORS[-1];
+my $oldversion = $MAJORS[0];
+
+use Test::More tests => 22;
+use PgCommon;
+
+my $testuser = 'postgres';
+
+# pg_createcluster and pg_ctlcluster
+is ((exec_as $testuser, "pg_createcluster $version main --start"), 0,
+ "pg_createcluster succeeds as user $testuser with appropriate owner permissions");
+
+like_program_out $testuser, 'pg_lsclusters -h', 0, qr/^$version\s+main.*online/m;
+like_program_out 'postgres', 'psql -Atl', 0, qr/template1.*UTF8/;
+
+# pg_dropcluster
+is ((exec_as $testuser, "pg_dropcluster $version main --stop"), 0,
+ "pg_dropcluster succeeds as user $testuser with appropriate directory owner permissions");
+
+# pg_upgradecluster
+SKIP: {
+ skip 'Only one major version installed, skipping pg_upgradecluster tests', 8 if ($oldversion eq $version);
+
+ is ((exec_as $testuser, "pg_createcluster $oldversion main --start"), 0,
+ "pg_createcluster succeeds as user $testuser with appropriate group permissions");
+ my $outref;
+ is ((exec_as $testuser, "pg_upgradecluster -v $version $oldversion main", $outref, 0), 0,
+ "pg_upgradecluster succeeds as user $testuser");
+ like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+ like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+
+ like_program_out $testuser, 'pg_lsclusters -h', 0,
+ qr/^$oldversion\s+main.*down.*\n^$version\s+main.*online/m;
+
+ # clean up
+ is ((exec_as $testuser, "pg_dropcluster $oldversion main"), 0);
+ is ((exec_as $testuser, "pg_dropcluster $version main --stop"), 0);
+}
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/135_pg_virtualenv.t b/t/135_pg_virtualenv.t
new file mode 100644
index 0000000..1662e5b
--- /dev/null
+++ b/t/135_pg_virtualenv.t
@@ -0,0 +1,35 @@
+# check if pg_virtualenv runs ok, even under fakeroot
+
+use strict;
+use warnings;
+
+use lib 't';
+use TestLib;
+
+use Test::More tests => 12 * @MAJORS + 8;
+
+foreach my $v (@MAJORS) {
+ my $args = 'sh -c \'echo "id|$(id -un)"; psql -AtXxc "SELECT current_user"\'';
+ my $virtualenv = "pg_virtualenv -v $v $args";
+
+ $ENV{USER} = 'root';
+ like_program_out 'root', $virtualenv, 0, qr!id.root\ncurrent_user.postgres!, "running pg_virtualenv as root";
+ $ENV{USER} = 'postgres';
+ like_program_out 'postgres', $virtualenv, 0, qr!id.postgres\ncurrent_user.postgres!, "running pg_virtualenv as postgres";
+ $ENV{USER} = 'nobody';
+ like_program_out 'nobody', $virtualenv, 0, qr!id.nobody\ncurrent_user.nobody!, "running pg_virtualenv as nobody";
+
+ SKIP: {
+ skip "/usr/bin/fakeroot not available", 6 unless (-x "/usr/bin/fakeroot"); # CentOS doesn't have fakeroot
+ $ENV{USER} = 'root';
+ like_program_out 'root', "fakeroot $virtualenv", 0, qr!id.root\ncurrent_user.postgres!, "running fakeroot pg_virtualenv as root";
+ $ENV{USER} = 'postgres';
+ like_program_out 'postgres', "fakeroot $virtualenv", 0, qr!id.root\ncurrent_user.postgres!, "running fakeroot pg_virtualenv as postgres";
+ $ENV{USER} = 'nobody';
+ like_program_out 'nobody', "fakeroot $virtualenv", 0, qr!id.root\ncurrent_user.nobody!, "running fakeroot pg_virtualenv as nobody";
+ }
+}
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/140_pg_config.t b/t/140_pg_config.t
new file mode 100644
index 0000000..770c75c
--- /dev/null
+++ b/t/140_pg_config.t
@@ -0,0 +1,89 @@
+# Check pg_config output
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => 14 * @MAJORS + ($PgCommon::rpm ? 1 : 2) * 12;
+
+my $multiarch = '';
+unless ($PgCommon::rpm) {
+ $multiarch = `dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null`;
+ chomp $multiarch;
+}
+note "Multiarch is " . ($multiarch ? 'enabled' : 'disabled');
+
+my $version;
+foreach $version (@MAJORS) {
+ note "checking version specific output for $version";
+ if ($version < '8.2') {
+ pass "Skipping known-broken pg_config check for version $version";
+ for (my $i = 0; $i < 13; ++$i) { pass '...'; }
+ next;
+ }
+ is_program_out 'postgres', "$PgCommon::binroot$version/bin/pg_config --pgxs", 0,
+ "$PgCommon::binroot$version/lib/pgxs/src/makefiles/pgxs.mk\n";
+ my $libdir = "/usr/lib" . ($version >= 9.3 and $multiarch ? "/$multiarch" : "") . "\n";
+ $libdir = "$PgCommon::binroot$version/lib\n" if ($PgCommon::rpm);
+ is_program_out 'postgres', "$PgCommon::binroot$version/bin/pg_config --libdir", 0,
+ $libdir;
+ is_program_out 'postgres', "$PgCommon::binroot$version/bin/pg_config --pkglibdir", 0,
+ "$PgCommon::binroot$version/lib\n";
+ is_program_out 'postgres', "$PgCommon::binroot$version/bin/pg_config --bindir", 0,
+ "$PgCommon::binroot$version/bin\n";
+ # mkdir should be in /bin on Debian. If /bin was linked to /usr/bin at build time, usrmerge was installed
+ SKIP: {
+ skip 'MKDIR_P not present before 9.0', 2 if ($version < 9.0);
+ skip 'MKDIR_P not checked on RedHat', 2 if ($PgCommon::rpm); # varies across builds/versions
+ is_program_out 'postgres', "grep ^MKDIR_P $PgCommon::binroot$version/lib/pgxs/src/Makefile.global", 0,
+ "MKDIR_P = /bin/mkdir -p\n";
+ }
+ SKIP: {
+ skip 'build path not canonicalized on RedHat', 4 if ($PgCommon::rpm);
+ my $pkgversion = `dpkg-query -f '\${Version}' -W postgresql-server-dev-$version`;
+ # check that we correctly canonicalized the build paths
+ SKIP: {
+ skip 'abs_top_builddir introduced in 9.5', 2 if ($version < 9.5);
+ skip 'abs_top_builddir not patched in Debian (old)stable', 2 if ($version < 10 and $pkgversion !~ /pgdg/);
+ is_program_out 'postgres', "grep ^abs_top_builddir $PgCommon::binroot$version/lib/pgxs/src/Makefile.global", 0,
+ "abs_top_builddir = /build/postgresql-$version$ENV{PG_FLAVOR}/build\n";
+ }
+ SKIP: {
+ skip 'abs_top_srcdir not patched before 9.3', 2 if ($version < 9.3);
+ skip 'abs_top_srcdir not patched in Debian (old)stable', 2 if ($version < 10 and $pkgversion !~ /pgdg/);
+ is_program_out 'postgres', "grep ^abs_top_srcdir $PgCommon::binroot$version/lib/pgxs/src/Makefile.global", 0,
+ "abs_top_srcdir = /build/postgresql-$version$ENV{PG_FLAVOR}/build/..\n";
+ }
+ }
+}
+
+my @pg_configs = $PgCommon::rpm ? qw(pg_config) : qw(pg_config pg_config.libpq-dev);
+for my $pg_config (@pg_configs) {
+ if ($pg_config eq 'pg_config' or $PgCommon::rpm) { # pg_config should point at newest installed postgresql-server-dev-$version
+ $version = $ALL_MAJORS[-1];
+ } else { # pg_config.libpq-dev should point at postgresql-server-dev-$(version of libpq-dev)
+ my $libpqdev_version = `dpkg-query --showformat '\${Version}' --show libpq-dev`;
+ $libpqdev_version =~ /^([89].\d|1.)/ or die "could not determine libpq-dev version";
+ $version = $1;
+ }
+ note "checking $pg_config output (should behave like version $version)";
+
+ SKIP: {
+ my $pgc = "$PgCommon::binroot$version/bin/pg_config";
+ skip "$pgc not installed, can't check full $pg_config output", 2 unless (-x $pgc);
+ my $full_output = `$pgc`;
+ is_program_out 'postgres', "$pg_config", 0, $full_output;
+ }
+ like_program_out 'postgres', "$pg_config --help", 0, qr/--includedir-server/;
+ is_program_out 'postgres', "$pg_config --pgxs", 0,
+ "$PgCommon::binroot$version/lib/pgxs/src/makefiles/pgxs.mk\n";
+ my $libdir = "/usr/lib" . ($version >= 9.3 and $multiarch ? "/$multiarch" : "") . "\n";
+ $libdir = "$PgCommon::binroot$version/lib\n" if ($PgCommon::rpm);
+ is_program_out 'postgres', "$pg_config --libdir", 0,
+ $libdir;
+ is_program_out 'postgres', "$pg_config --pkglibdir", 0,
+ "$PgCommon::binroot$version/lib\n";
+ is_program_out 'postgres', "$pg_config --bindir", 0,
+ "$PgCommon::binroot$version/bin\n";
+}
diff --git a/t/150_tsearch_stemming.t b/t/150_tsearch_stemming.t
new file mode 100644
index 0000000..a2a6253
--- /dev/null
+++ b/t/150_tsearch_stemming.t
@@ -0,0 +1,108 @@
+# Check tsearch, and stemming with dynamic creation of .affix/.dict files
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+my $version = $MAJORS[-1];
+
+use Test::More tests => ($MAJORS[-1] < 8.3 or $PgCommon::rpm) ? 1 : 37;
+if ($version < 8.3) {
+ pass 'tsearch dictionaries not tested before 8.3';
+ exit;
+}
+if ($PgCommon::rpm) {
+ pass 'tsearch dictionaries not handled by postgresql-common on RedHat';
+ exit;
+}
+
+# test pg_updatedicts
+unlink '/var/cache/postgresql/dicts/en_us.affix';
+unlink '/var/cache/postgresql/dicts/en_us.dict';
+unlink "/usr/share/postgresql/$version/tsearch_data/en_us.affix";
+unlink "/usr/share/postgresql/$version/tsearch_data/en_us.dict";
+is ((exec_as 0, 'pg_updatedicts'), 0, 'pg_updatedicts succeeded');
+ok -f '/var/cache/postgresql/dicts/en_us.affix',
+ 'pg_updatedicts created en_us.affix';
+ok -f '/var/cache/postgresql/dicts/en_us.dict',
+ 'pg_updatedicts created en_us.dict';
+ok -l "/usr/share/postgresql/$version/tsearch_data/en_us.affix",
+ "pg_updatedicts created $version en_us.affix symlink";
+ok -l "/usr/share/postgresql/$version/tsearch_data/en_us.dict",
+ "pg_updatedicts created $version en_us.dict symlink";
+
+# create cluster
+is ((system "pg_createcluster $version main --start >/dev/null"), 0, "pg_createcluster $version main");
+
+# create DB with en_US text search configuration
+is_program_out 'postgres', 'createdb fts', 0, '';
+
+my $outref;
+
+is ((exec_as 'postgres', 'psql -qd fts -c "
+ CREATE TEXT SEARCH CONFIGURATION public.sc_english ( COPY = pg_catalog.english );
+ CREATE TEXT SEARCH DICTIONARY english_ispell (TEMPLATE = ispell, DictFile = en_US,
+ AffFile = en_US, StopWords = english);
+ SET default_text_search_config = \'public.sc_english\';
+ ALTER TEXT SEARCH CONFIGURATION public.sc_english
+ ALTER MAPPING FOR asciiword WITH english_ispell, english_stem;"', $outref),
+ 0, 'creating en_US full text search configuration ' . $$outref);
+
+# create test table and index
+my $outref;
+is ((exec_as 'postgres', 'psql -qd fts -c "
+ CREATE TABLE stuff (id SERIAL PRIMARY KEY, text TEXT, textsearch tsvector);
+ UPDATE stuff SET textsearch = to_tsvector(\'public.sc_english\', coalesce(text, \'\'));
+ CREATE INDEX textsearch_idx ON stuff USING gin(textsearch);
+ CREATE TRIGGER textsearch_update_trigger BEFORE INSERT OR UPDATE
+ ON stuff FOR EACH ROW EXECUTE PROCEDURE
+ tsvector_update_trigger(textsearch, \'public.sc_english\', text);
+ INSERT INTO stuff (text) VALUES (\'PostgreSQL rocks\');
+ INSERT INTO stuff (text) VALUES (\'Linux rocks\');
+ INSERT INTO stuff (text) VALUES (\'I am your father\'\'s nephew\'\'s former roommate\');
+ INSERT INTO stuff (text) VALUES (\'3 cafés\');
+ "'), 0, 'creating data table and search index');
+
+# test stemming
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT dictionary, lexemes FROM ts_debug(\'public.sc_english\', \'friendliest\')"',
+ 0, "english_ispell|{friendly}\n", 'stem search of correct word';
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT dictionary, lexemes FROM ts_debug(\'public.sc_english\', \'father\'\'s\')"',
+ 0, "english_ispell|{father}\n|\nenglish_ispell|{}\n", 'stem search of correct word';
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT dictionary, lexemes FROM ts_debug(\'public.sc_english\', \'duffles\')"',
+ 0, "english_stem|{duffl}\n", 'stem search of unknown word';
+
+# test searching
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT text FROM stuff, to_tsquery(\'rocks\') query WHERE query @@ to_tsvector(text)"',
+ 0, "PostgreSQL rocks\nLinux rocks\n", 'full text search, exact word';
+
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT text FROM stuff, to_tsquery(\'rock\') query WHERE query @@ to_tsvector(text)"',
+ 0, "PostgreSQL rocks\nLinux rocks\n", 'full text search for word stem';
+
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT text FROM stuff, to_tsquery(\'roc\') query WHERE query @@ to_tsvector(text)"',
+ 0, '', 'full text search for word substring fails';
+
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT text FROM stuff, to_tsquery(\'cafés\') query WHERE query @@ to_tsvector(text)"',
+ 0, "3 cafés\n", 'full text search, exact unicode word';
+
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT text FROM stuff, to_tsquery(\'café\') query WHERE query @@ to_tsvector(text)"',
+ 0, "3 cafés\n", 'full text search for unicode word stem';
+
+is_program_out 'postgres',
+ 'psql -Atd fts -c "SELECT text FROM stuff, to_tsquery(\'afé\') query WHERE query @@ to_tsvector(text)"',
+ 0, '', 'full text search for unicode word substring fails';
+
+# clean up
+is ((system "pg_dropcluster $version main --stop"), 0);
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/160_alternate_confroot.t b/t/160_alternate_confroot.t
new file mode 100644
index 0000000..e2a9604
--- /dev/null
+++ b/t/160_alternate_confroot.t
@@ -0,0 +1,57 @@
+# Check that we can do all operations using a per-user $PG_CLUSTER_CONF_ROOT
+
+use strict;
+
+use lib 't';
+use TestLib;
+
+my $version = $MAJORS[0];
+
+use Test::More tests => 28;
+
+# prepare nobody-owned root dir for $PG_CLUSTER_CONF_ROOT
+my $rootdir=`su -s /bin/sh -c 'mktemp -d' nobody`;
+chomp $rootdir;
+($rootdir) = $rootdir =~ m!^([a-zA-Z0-9._/]+)$!; # untaint
+$ENV{'PG_CLUSTER_CONF_ROOT'} = "$rootdir/etc";
+
+is ((exec_as 'nobody', "pg_createcluster $version test -d $rootdir/data/test -l $rootdir/test.log --start"), 0);
+
+is_program_out 'nobody', 'env -u PG_CLUSTER_CONF_ROOT pg_lsclusters -h', 0, '';
+like_program_out 'nobody', "pg_lsclusters -h", 0,
+ qr!^$version\s+test.*online\s+nobody\s+$rootdir/data/test\s+$rootdir/test.log$!;
+
+like_program_out 'nobody', "psql -Atl", 0, qr/template1.*UTF8/;
+
+# pg_upgradecluster
+if ($MAJORS[0] ne $MAJORS[-1]) {
+ my $outref;
+ is ((exec_as 'nobody', "pg_upgradecluster --logfile $rootdir/testupgr.log -v $MAJORS[-1] $version test $rootdir/data/testupgr", $outref, 0), 0);
+ like $$outref, qr/Starting upgraded cluster/, 'pg_upgradecluster reported cluster startup';
+ like $$outref, qr/Success. Please check/, 'pg_upgradecluster reported successful operation';
+
+ like_program_out 'nobody', 'pg_lsclusters -h', 0,
+ qr!^$version\s+test.*down.*\n^$MAJORS[-1]\s+test.*online\s+nobody\s+$rootdir/data/testupgr\s+$rootdir/testupgr.log$!m;
+
+ # clean up
+ is_program_out 'nobody', "pg_dropcluster $version test", 0, '';
+ is_program_out 'nobody', "pg_dropcluster $MAJORS[-1] test --stop", 0, '';
+} else {
+ pass 'Only one major version installed, skipping pg_upgradecluster tests';
+ for (my $i = 0; $i < 6; ++$i) { pass '...'; }
+
+ is_program_out 'nobody', "pg_dropcluster $version test --stop", 0, '';
+}
+
+# pg_dropcluster
+is_program_out 'nobody', "pg_lsclusters -h", 0, '';
+
+ok_dir "$rootdir/data", [], 'No files in root/data left behind';
+ok_dir "$rootdir", ['etc', 'data'], 'No cruft in root dir left behind';
+
+system "rm -rf $rootdir";
+
+delete $ENV{'PG_CLUSTER_CONF_ROOT'};
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/170_extensions.t b/t/170_extensions.t
new file mode 100644
index 0000000..1abb330
--- /dev/null
+++ b/t/170_extensions.t
@@ -0,0 +1,87 @@
+# Check that all extensions install successfully.
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More 0.87; # needs libtest-simple-perl backport on lenny
+
+foreach my $v (@MAJORS) {
+note "Running tests for $v";
+
+if ($v < '9.1') {
+ pass 'No extensions for version < 9.1';
+ next;
+}
+
+# create cluster
+is ((system "pg_createcluster $v main --start >/dev/null"), 0, "pg_createcluster $v main");
+
+# plpgsql is installed by default
+is_program_out 'postgres', "psql -Atc 'SELECT extname FROM pg_extension'", 0, "plpgsql\n";
+
+my %depends = (
+ bool_plperl => [qw(plperl)],
+ bool_plperlu => [qw(plperlu)],
+ earthdistance => [qw(cube)],
+ hstore_plperl => [qw(hstore plperl)],
+ hstore_plperlu => [qw(hstore plperlu)],
+ hstore_plpython2u => [qw(hstore plpython2u)],
+ hstore_plpython3u => [qw(hstore plpython3u)],
+ hstore_plpythonu => [qw(hstore plpythonu)],
+ jsonb_plperl => [qw(plperl)], # PG 11
+ jsonb_plperlu => [qw(plperlu)], # PG 11
+ jsonb_plpython2u => [qw(plpython2u)], # PG 11
+ jsonb_plpython3u => [qw(plpython3u)], # PG 11
+ jsonb_plpythonu => [qw(plpythonu)], # PG 11
+ ltree_plpython2u => [qw(ltree plpython2u)],
+ ltree_plpython3u => [qw(ltree plpython3u)],
+ ltree_plpythonu => [qw(ltree plpythonu)],
+);
+
+foreach (</usr/share/postgresql/$v/extension/*.control>) {
+ my ($extname) = $_ =~ /^.*\/(.*)\.control$/;
+ next if ($extname eq 'plpgsql');
+
+ if ($depends{$extname}) {
+ for my $dep (@{$depends{$extname}}) {
+ is_program_out 'postgres', "psql -qc 'CREATE EXTENSION $dep'", 0, '',
+ "$extname dependency $dep installs without error";
+ }
+ }
+
+ if ($extname eq 'hstore' && $v eq '9.1') {
+ # EXFAIL: hstore in 9.1 throws a warning about obsolete => operator
+ like_program_out 'postgres', "psql -qc 'CREATE EXTENSION \"$extname\"'", 0,
+ qr/=>/, "extension $extname installs (with warning)";
+ } elsif ($extname eq 'chkpass' && $v >= '9.5') {
+ # chkpass is slightly broken, see
+ # http://www.postgresql.org/message-id/20141117162116.GA3565@msg.df7cb.de
+ like_program_out 'postgres', "psql -qc 'CREATE EXTENSION \"$extname\"'", 0,
+ qr/WARNING: type input function chkpass_in should not be volatile/,
+ "extension $extname installs (with warning)";
+ } else {
+ is_program_out 'postgres', "psql -qc 'CREATE EXTENSION \"$extname\"'", 0, '',
+ "extension $extname installs without error";
+ }
+
+ is_program_out 'postgres', "psql -qc 'DROP EXTENSION \"$extname\"'", 0, '',
+ "extension $extname removes without error";
+
+ if ($depends{$extname}) {
+ for my $dep (@{$depends{$extname}}) {
+ is_program_out 'postgres', "psql -qc 'DROP EXTENSION $dep'", 0, '',
+ "$extname dependency extension $dep removes without error";
+ }
+ }
+}
+
+# clean up
+is ((system "pg_dropcluster $v main --stop"), 0, "pg_dropcluster $v main");
+check_clean;
+}
+
+done_testing();
+
+# vim: filetype=perl
diff --git a/t/180_ecpg.t b/t/180_ecpg.t
new file mode 100644
index 0000000..5d3f7a8
--- /dev/null
+++ b/t/180_ecpg.t
@@ -0,0 +1,56 @@
+# Check that ecpg works
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => 14;
+
+my $v = $MAJORS[-1];
+
+# prepare nobody-owned work dir
+my $workdir=`su -s /bin/sh -c 'mktemp -d' nobody`;
+chomp $workdir;
+chdir $workdir or die "could not chdir to $workdir: $!";
+
+# create test code
+open F, '>test.pgc' or die "Could not open $workdir/test.pgc: $!";
+print F <<EOF;
+#include <stdio.h>
+#include <stdlib.h>
+
+EXEC SQL WHENEVER SQLWARNING SQLPRINT;
+EXEC SQL WHENEVER SQLERROR SQLPRINT;
+
+EXEC SQL BEGIN DECLARE SECTION;
+ char output[1024];
+EXEC SQL END DECLARE SECTION;
+
+int main() {
+ ECPGdebug(1, stderr);
+ EXEC SQL CONNECT TO template1;
+ EXEC SQL SELECT 'Database is ' || current_database() INTO :output;
+ puts(output);
+ EXEC SQL DISCONNECT ALL;
+ return 0;
+}
+EOF
+close F;
+chmod 0644, 'test.pgc';
+
+is_program_out 'nobody', 'ecpg test.pgc', 0, '', 'ecpg processing';
+
+is_program_out 'nobody', 'cc -I$(pg_config --includedir) -L$(pg_config --libdir) -o test test.c -lecpg',
+ 0, '', 'compiling ecpg output';
+chdir '/' or die "could not chdir to /: $!";
+
+# run program
+like_program_out 'nobody', "pg_virtualenv $workdir/test", 0, qr/Database is template1/,
+ 'test program runs and gives correct output';
+
+# clean up
+system "rm -rf $workdir";
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/190_pg_buildext.t b/t/190_pg_buildext.t
new file mode 100644
index 0000000..9a50712
--- /dev/null
+++ b/t/190_pg_buildext.t
@@ -0,0 +1,104 @@
+# Check pg_buildext and that our debhelper integration works
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More;
+
+if ($PgCommon::rpm) {
+ pass 'No pg_buildext tests on RedHat';
+ done_testing();
+ exit;
+}
+
+# when invoked from the postgresql-NN package tests, postgresql-server-dev-all is not installed
+if (! -x '/usr/bin/dh_make_pgxs') {
+ pass "Skipping pg_buildext tests, /usr/bin/dh_make_pgxs is not installed";
+ done_testing();
+ exit;
+}
+
+my $arch = `dpkg-architecture -qDEB_HOST_ARCH`;
+chomp $arch;
+
+if ($ENV{PG_VERSIONS}) {
+ note "PG_VERSIONS=$ENV{PG_VERSIONS}";
+ $ENV{PG_SUPPORTED_VERSIONS} = join ' ', (grep { $_ >= 9.1 } split /\s+/, $ENV{PG_VERSIONS});
+ unless ($ENV{PG_SUPPORTED_VERSIONS}) {
+ ok 1, 'No versions with extension support to test';
+ done_testing();
+ exit;
+ }
+ note "PG_SUPPORTED_VERSIONS=$ENV{PG_SUPPORTED_VERSIONS}";
+}
+my @versions = split /\s+/, `/usr/share/postgresql-common/supported-versions`;
+
+# prepare build environment
+chmod 0777, 't/foo', 't/foo/foo-123', 't/bar/debian';
+umask 0022;
+chdir 't/foo';
+
+program_ok 0, 'make clean';
+program_ok 'nobody', 'make tar';
+program_ok 'nobody', 'cd foo-123 && echo y | EDITOR=true dh_make_pgxs';
+
+note "testing 'dh --with pgxs'";
+program_ok 'nobody', 'cd foo-123 && DEB_BUILD_OPTIONS=nocheck dpkg-buildpackage -us -uc';
+
+foreach my $ver (@versions) {
+ my $deb = "postgresql-$ver-foo_123-1_$arch.deb";
+ ok (-f $deb, "$deb was built");
+ SKIP: {
+ my $have_extension_destdir = `grep extension_destdir /usr/share/postgresql/$ver/postgresql.conf.sample`;
+ skip "No in-tree installcheck on PG $ver (missing extension_destdir)", 2 unless ($have_extension_destdir);
+ like_program_out 'nobody', "cd foo-123 && PG_SUPPORTED_VERSIONS=$ver dh_pgxs_test",
+ 0, qr/PostgreSQL $ver installcheck.*(test foo * \.\.\. ok|ok 1 * - foo)/s; # old/new PG 16 syntax
+ }
+ program_ok 0, "dpkg -i $deb";
+ like_program_out 'nobody', "cd foo-123 && pg_buildext installcheck",
+ 0, qr/PostgreSQL $ver installcheck.*(test foo * \.\.\. ok|ok 1 * - foo)/s;
+ like_program_out 'nobody', "cd foo-123 && echo 'SELECT 3*41, version()' | pg_buildext psql", 0, qr/123.*PostgreSQL $ver/;
+ like_program_out 'nobody', "cd foo-123 && echo 'echo --\$PGVERSION--' | pg_buildext virtualenv", 0, qr/--$ver--/;
+ like_program_out 'nobody', "cd foo-123 && pg_buildext run echo --%v--", 0, qr/--$ver--/;
+ program_ok 0, "dpkg -r postgresql-$ver-foo";
+}
+
+note "testing 'dh --with pgxs_loop'";
+system "rm -f postgresql-*.deb";
+
+program_ok 'nobody', 'sed -i -e s/pgxs/pgxs_loop/ foo-123/debian/rules';
+program_ok 'nobody', 'cd foo-123 && DEB_BUILD_OPTIONS=nocheck dpkg-buildpackage -us -uc';
+
+foreach my $ver (@versions) {
+ my $deb = "postgresql-$ver-foo_123-1_$arch.deb";
+ ok (-f $deb, "$deb was built");
+}
+
+program_ok 'nobody', 'make clean';
+
+note "testing pg_buildext updatecontrol";
+chdir '../bar';
+program_ok 'nobody', 'PG_SUPPORTED_VERSIONS="0.9 1.0 1.1 2 3" pg_buildext updatecontrol';
+is `cat debian/control`, "Source: bar
+Build-Depends: whatever, postgresql-1.0-moo (>= 1), postgresql-1.1-moo (>= 1), postgresql-2-moo (>= 1), more,
+ postgresql-2-new,
+ postgresql-1.0, postgresql-1.1, postgresql-2
+
+Package: postgresql-1.0-bar
+Architecture: some
+Depends: postgresql-1.0-moo
+
+Package: postgresql-1.1-bar
+Architecture: some
+Depends: postgresql-1.1-moo
+
+Package: postgresql-2-bar
+Architecture: some
+Depends: postgresql-2-moo
+", "PGVERSION and PGVERSIONS were correctly replaced";
+
+done_testing();
+
+# vim: filetype=perl
diff --git a/t/200_maintscripts.t b/t/200_maintscripts.t
new file mode 100644
index 0000000..3fc5018
--- /dev/null
+++ b/t/200_maintscripts.t
@@ -0,0 +1,46 @@
+# This test runs last since we are reinstalling postgresql-common, and we want
+# to avoid spoiling the other tests with any version skew.
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+
+use Test::More tests => $PgCommon::rpm ? 1 : 15;
+
+if ($PgCommon::rpm) {
+ pass 'No maintainer script tests on rpm';
+ exit;
+}
+
+my $v = $MAJORS[-1];
+
+note -d "/run/systemd/system" ? "We are running systemd" : "We are not running systemd";
+
+# create cluster
+program_ok 0, "pg_createcluster $v main --start";
+
+# get postmaster PID
+my $postmaster_pid = `head -1 /var/lib/postgresql/$v/main/postmaster.pid`;
+chomp $postmaster_pid;
+ok $postmaster_pid > 0, "postmaster PID is $postmaster_pid";
+
+# "upgrade" postgresql-common to check if postgresql.service is left alone
+program_ok 0, 'apt-get update -q', 0, '';
+note `apt-cache policy postgresql-common`;
+program_ok 0, 'apt-get install -y --reinstall -o DPkg::Options::=--force-confnew postgresql-common', 0, '';
+note `apt-cache policy postgresql-common`;
+
+# get postmaster PID again, compare
+my $postmaster_pid2 = `head -1 /var/lib/postgresql/$v/main/postmaster.pid`;
+chomp $postmaster_pid2;
+ok $postmaster_pid2 > 0, "postmaster PID is $postmaster_pid2";
+is $postmaster_pid, $postmaster_pid2, "postmaster was not restarted";
+
+# stop server, clean up, check for leftovers
+program_ok 0, "pg_dropcluster $v main --stop";
+
+check_clean;
+
+# vim: filetype=perl
diff --git a/t/TestLib.pm b/t/TestLib.pm
new file mode 100644
index 0000000..408c9f5
--- /dev/null
+++ b/t/TestLib.pm
@@ -0,0 +1,270 @@
+# Common functionality for postgresql-common self tests
+#
+# (C) 2005-2009 Martin Pitt <mpitt@debian.org>
+# (C) 2013-2022 Christoph Berg <myon@debian.org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+package TestLib;
+use strict;
+use Exporter;
+use Test::More;
+use PgCommon qw/get_versions change_ugid next_free_port/;
+
+our $VERSION = 1.00;
+our @ISA = ('Exporter');
+our @EXPORT = qw/os_release ps ok_dir exec_as deb_installed rpm_installed package_version
+ version_ge program_ok is_program_out like_program_out unlike_program_out
+ pidof pid_env check_clean
+ @ALL_MAJORS @MAJORS $delay/;
+
+our @ALL_MAJORS = get_versions(); # not affected by PG_VERSIONS/-v
+our @MAJORS = $ENV{PG_VERSIONS} ? split (/\s+/, $ENV{PG_VERSIONS}) : @ALL_MAJORS;
+our $delay = 500_000; # 500ms
+
+# called if a test fails; spawn a shell if the environment variable
+# FAILURE=shell is set
+sub fail_debug {
+ if ($ENV{'FAILURE'} eq 'shell') {
+ if ((system 'bash') != 0) {
+ exit 1;
+ }
+ }
+}
+
+# parse /etc/os-release and return (os, version number)
+sub os_release {
+ open OS, "/etc/os-release" or return (undef, undef);
+ my ($os, $osversion);
+ while (<OS>) {
+ $os = $1 if /^ID=(.*)/;
+ $osversion = $1 if /^VERSION_ID="?([^"]*)/;
+ }
+ close OS;
+ $osversion = 'unstable' if ($os eq 'debian' and not defined $osversion);
+ return ($os, $osversion);
+}
+
+# Return whether a given deb is installed.
+# Arguments: <deb name>
+sub deb_installed {
+ open (DPKG, "dpkg -s $_[0] 2>/dev/null|") or die "call dpkg: $!";
+ my $result = 0;
+ while (<DPKG>) {
+ if (/^Status: install ok installed/) {
+ $result = 1;
+ last;
+ }
+ }
+ close DPKG;
+
+ return $result;
+}
+
+# Return whether a given rpm is installed.
+# Arguments: <rpm name>
+sub rpm_installed {
+ open (RPM, "rpm -qa $_[0] 2>/dev/null|") or die "call rpm: $!";
+ my $out = <RPM>; # returns void or the package name
+ close RPM;
+ return ($out =~ /./);
+}
+
+# Return a package version
+# Arguments: <package>
+sub package_version {
+ my $package = shift;
+ if ($PgCommon::rpm) {
+ return `rpm --queryformat '%{VERSION}' -q $package`;
+ } else {
+ my $version = `dpkg-query -f '\${Version}' --show $package`;
+ chomp $version;
+ return $version;
+ }
+}
+
+# Return whether a version is greater or equal to another one
+# Arguments: <ver1> <ver2>
+sub version_ge {
+ my ($v1, $v2) = @_;
+ use IPC::Open2;
+ open2(\*CHLD_OUT, \*CHLD_IN, 'sort', '-Vr');
+ print CHLD_IN "$v1\n";
+ print CHLD_IN "$v2\n";
+ close CHLD_IN;
+ my $v_ge = <CHLD_OUT>;
+ chomp $v_ge;
+ return $v_ge eq $v1;
+}
+
+# Return the user, group, and command line of running processes for the given
+# program.
+sub ps {
+ return `ps h -o user,group,args -C $_[0] | grep '$_[0]' | sort -u`;
+}
+
+# Return array of pids that match the given command name (we require a leading
+# slash so the postgres children are filtered out)
+sub pidof {
+ my $prg = shift;
+ open F, '-|', 'ps', 'h', '-C', $prg, '-o', 'pid,cmd' or die "open: $!";
+ my @pids;
+ while (<F>) {
+ if ((index $_, "/$prg") >= 0) {
+ push @pids, (split)[0];
+ }
+ }
+ close F;
+ return @pids;
+}
+
+# Return an reference to an array of all entries but . and .. of the given directory.
+sub dircontent {
+ my $dir = $_[0];
+ opendir D, $dir or return ["opendir $dir: $!"];
+ my @e = grep { $_ ne '.' && $_ ne '..' } readdir (D);
+ closedir D;
+ return [sort @e];
+}
+
+# Return environment of given PID
+sub pid_env {
+ my ($user, $pid) = @_;
+ my $path = "/proc/$pid/environ";
+ my @lines;
+ open E, "su -c 'cat $path' $user |" or warn "open $path: $!";
+ {
+ local $/;
+ @lines = split '\0', <E>;
+ }
+ close E;
+ my %env;
+ foreach (@lines) {
+ my ($k, $v) = (split '=');
+ $env{$k} = $v;
+ }
+ return %env;
+}
+
+# Check the contents of a directory.
+# Arguments: <directory name> <ref to expected dir content> <test description>
+sub ok_dir {
+ my $content = dircontent $_[0];
+ if (eq_set $content, $_[1]) {
+ pass $_[2];
+ } else {
+ diag "Expected directory contents: [@{$_[1]}], actual contents: [@$content]\n";
+ fail $_[2];
+ }
+}
+
+# Execute a command as a different user and return the output. Prints the
+# output of the command if exit code differs from expected one.
+# Arguments: <user> <system command> <ref to output> [<expected exit code>]
+# Returns: Program exit code
+sub exec_as {
+ my $uid;
+ if ($_[0] =~ /\d+/) {
+ $uid = int($_[0]);
+ } else {
+ $uid = getpwnam $_[0];
+ defined($uid) or die "TestLib::exec_as: target user '$_[0]' does not exist";
+ }
+ change_ugid ($uid, (getpwuid $uid)[3]);
+ die "changing euid: $!" if $> != $uid;
+ my $out = `$_[1] 2>&1`;
+ my $result = $? >> 8;
+ $< = $> = 0;
+ $( = $) = 0;
+ die "changing euid back to root: $!" if $> != 0;
+ $_[2] = \$out;
+
+ if (defined $_[3] && $_[3] != $result) {
+ print "command '$_[1]' did not exit with expected code $_[3] but with $result:\n";
+ print $out;
+ fail_debug;
+ }
+ return $result;
+}
+
+# Execute a command as a particular user, and check the exit code
+# Arguments: <user> <command> [<expected exit code>] [<description>]
+sub program_ok {
+ my ($user, $cmd, $exit, $description) = @_;
+ $exit ||= 0;
+ $description ||= $cmd;
+ my $outref;
+ ok ((exec_as $user, $cmd, \$outref, $exit) == $exit, $description);
+}
+
+# Execute a command as a particular user, and check the exit code and output
+# (merged stdout/stderr).
+# Arguments: <user> <command> <expected exit code> <expected output> [<description>]
+sub is_program_out {
+ my $outref;
+ my $result = exec_as $_[0], $_[1], $outref;
+ is $result, $_[2], $_[1] or fail_debug;
+ is ($$outref, $_[3], (defined $_[4] ? $_[4] : "correct output of $_[1]")) or fail_debug;
+}
+
+# Execute a command as a particular user, and check the exit code and output
+# against a regular expression (merged stdout/stderr).
+# Arguments: <user> <command> <expected exit code> <expected output re> [<description>]
+sub like_program_out {
+ my $outref;
+ my $result = exec_as $_[0], $_[1], $outref;
+ is $result, $_[2], $_[1] or fail_debug;
+ like ($$outref, $_[3], (defined $_[4] ? $_[4] : "correct output of $_[1]")) or fail_debug;
+}
+
+# Execute a command as a particular user, check the exit code, and check that
+# the output does not match a regular expression (merged stdout/stderr).
+# Arguments: <user> <command> <expected exit code> <expected output re> [<description>]
+sub unlike_program_out {
+ my $outref;
+ my $result = exec_as $_[0], $_[1], $outref;
+ is $result, $_[2], $_[1] or fail_debug;
+ unlike ($$outref, $_[3], (defined $_[4] ? $_[4] : "correct output of $_[1]")) or fail_debug;
+}
+
+# Check that all PostgreSQL related directories are empty and no
+# postgres processes are running. Should be called at the end
+# of all tests. Does 8 tests.
+sub check_clean {
+ note "Cleanup";
+ is (`pg_lsclusters -h`, '', 'Cleanup: No clusters left behind');
+ is ((ps 'postgres'), '', 'No postgres processes left behind');
+
+ my @check_dirs = ('/etc/postgresql', '/var/lib/postgresql',
+ '/var/run/postgresql');
+ foreach (@check_dirs) {
+ if (-d) {
+ ok_dir $_, [], "No files in $_ left behind";
+ } else {
+ pass "Directory $_ does not exist";
+ }
+ }
+ # we always want /var/log/postgresql/ to exist, so that logrotate does not
+ # complain about missing directories
+ ok_dir '/var/log/postgresql', [], "No files in /var/log/postgresql left behind";
+
+ # prefer ss over netstat (until all debian/tests/control files in postgresql-* have been updated)
+ unless (-x '/bin/netstat' and not -x '/bin/ss') {
+ is `ss --no-header -tlp 'sport >= 5432 and sport <= 5439'`, '',
+ 'PostgreSQL TCP ports are closed';
+ } else {
+ is `netstat -avptn 2>/dev/null | grep ":543[2-9]\\b.*LISTEN"`, '',
+ 'PostgreSQL TCP ports are closed';
+ }
+ is next_free_port(), 5432, "Next free port is 5432";
+}
+
+1;
diff --git a/t/bar/debian/control.in b/t/bar/debian/control.in
new file mode 100644
index 0000000..86b8069
--- /dev/null
+++ b/t/bar/debian/control.in
@@ -0,0 +1,8 @@
+Source: bar
+Build-Depends: whatever, postgresql-PGVERSIONS-moo (>= 1), more,
+ postgresql-PGVERSION-new,
+ postgresql-PGVERSIONS
+
+Package: postgresql-PGVERSION-bar
+Architecture: some
+Depends: postgresql-PGVERSION-moo
diff --git a/t/bar/debian/pgversions b/t/bar/debian/pgversions
new file mode 100644
index 0000000..41f0ed3
--- /dev/null
+++ b/t/bar/debian/pgversions
@@ -0,0 +1,3 @@
+1.0
+1.1
+2
diff --git a/t/foo/Makefile b/t/foo/Makefile
new file mode 100644
index 0000000..75e95e1
--- /dev/null
+++ b/t/foo/Makefile
@@ -0,0 +1,12 @@
+TAR = foo_123.orig.tar.gz
+
+test: $(TAR)
+ cd foo-123 && echo y | EDITOR=true dh_make_pgxs
+ cd foo-123 && dpkg-buildpackage -us -uc
+
+tar $(TAR):
+ tar cfz $(TAR) foo-123/
+
+clean:
+ rm -f *.*
+ rm -rf foo-123/build-* foo-123/debian foo-123/*.o*
diff --git a/t/foo/foo-123/Makefile b/t/foo/foo-123/Makefile
new file mode 100644
index 0000000..9f9a2f8
--- /dev/null
+++ b/t/foo/foo-123/Makefile
@@ -0,0 +1,10 @@
+MODULE_big = foo
+OBJS = foo.o
+EXTENSION = foo
+DATA = foo--100.sql foo--100--123.sql foo--123.sql
+REGRESS = foo upgrade
+
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+
diff --git a/t/foo/foo-123/README.md b/t/foo/foo-123/README.md
new file mode 100644
index 0000000..14235f9
--- /dev/null
+++ b/t/foo/foo-123/README.md
@@ -0,0 +1,5 @@
+The PostgreSQL foo extension
+============================
+
+This extension exists for testing postgresql-common's `dh_make_pgxs` and
+`dh --with pgxs` mechanisms.
diff --git a/t/foo/foo-123/expected/foo.out b/t/foo/foo-123/expected/foo.out
new file mode 100644
index 0000000..62afef7
--- /dev/null
+++ b/t/foo/foo-123/expected/foo.out
@@ -0,0 +1,8 @@
+CREATE EXTENSION foo;
+SELECT foo();
+ foo
+-----
+ bar
+(1 row)
+
+DROP EXTENSION foo;
diff --git a/t/foo/foo-123/expected/upgrade.out b/t/foo/foo-123/expected/upgrade.out
new file mode 100644
index 0000000..98fc8e6
--- /dev/null
+++ b/t/foo/foo-123/expected/upgrade.out
@@ -0,0 +1,14 @@
+CREATE EXTENSION foo VERSION "100";
+SELECT foo();
+ foo
+---------
+ old bar
+(1 row)
+
+ALTER EXTENSION foo UPDATE TO "123";
+SELECT foo();
+ foo
+-----
+ bar
+(1 row)
+
diff --git a/t/foo/foo-123/foo--100--123.sql b/t/foo/foo-123/foo--100--123.sql
new file mode 100644
index 0000000..a6e7adc
--- /dev/null
+++ b/t/foo/foo-123/foo--100--123.sql
@@ -0,0 +1,3 @@
+CREATE OR REPLACE FUNCTION foo ()
+RETURNS text LANGUAGE C
+AS '$libdir/foo';
diff --git a/t/foo/foo-123/foo--100.sql b/t/foo/foo-123/foo--100.sql
new file mode 100644
index 0000000..5ed8546
--- /dev/null
+++ b/t/foo/foo-123/foo--100.sql
@@ -0,0 +1,3 @@
+CREATE OR REPLACE FUNCTION foo ()
+RETURNS text LANGUAGE SQL
+AS $$ SELECT 'old bar'::text $$;
diff --git a/t/foo/foo-123/foo--123.sql b/t/foo/foo-123/foo--123.sql
new file mode 100644
index 0000000..a6e7adc
--- /dev/null
+++ b/t/foo/foo-123/foo--123.sql
@@ -0,0 +1,3 @@
+CREATE OR REPLACE FUNCTION foo ()
+RETURNS text LANGUAGE C
+AS '$libdir/foo';
diff --git a/t/foo/foo-123/foo.c b/t/foo/foo-123/foo.c
new file mode 100644
index 0000000..8677acc
--- /dev/null
+++ b/t/foo/foo-123/foo.c
@@ -0,0 +1,13 @@
+#include "postgres.h"
+#include "fmgr.h"
+#include "utils/builtins.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1 (foo);
+
+Datum
+foo (PG_FUNCTION_ARGS)
+{
+ PG_RETURN_TEXT_P(cstring_to_text("bar"));
+}
diff --git a/t/foo/foo-123/foo.control b/t/foo/foo-123/foo.control
new file mode 100644
index 0000000..81c95c8
--- /dev/null
+++ b/t/foo/foo-123/foo.control
@@ -0,0 +1,2 @@
+default_version = '123'
+comment = 'The foo extension'
diff --git a/t/foo/foo-123/sql/foo.sql b/t/foo/foo-123/sql/foo.sql
new file mode 100644
index 0000000..155f63e
--- /dev/null
+++ b/t/foo/foo-123/sql/foo.sql
@@ -0,0 +1,5 @@
+CREATE EXTENSION foo;
+
+SELECT foo();
+
+DROP EXTENSION foo;
diff --git a/t/foo/foo-123/sql/upgrade.sql b/t/foo/foo-123/sql/upgrade.sql
new file mode 100644
index 0000000..b4427b4
--- /dev/null
+++ b/t/foo/foo-123/sql/upgrade.sql
@@ -0,0 +1,7 @@
+CREATE EXTENSION foo VERSION "100";
+
+SELECT foo();
+
+ALTER EXTENSION foo UPDATE TO "123";
+
+SELECT foo();
diff --git a/t/template b/t/template
new file mode 100644
index 0000000..fa1df59
--- /dev/null
+++ b/t/template
@@ -0,0 +1,26 @@
+# Check XXX
+
+use strict;
+
+use lib 't';
+use TestLib;
+use PgCommon;
+use Test::More tests => 14;
+
+my @versions = ($MAJORS[-1]);
+
+# create clusters
+foreach (@versions) {
+ is ((system "pg_createcluster $_ main --start >/dev/null"), 0, "pg_createcluster $_ main");
+ is_program_out 'postgres', "createdb --cluster $_/main XXX", 0, '';
+}
+
+# XXX
+
+# clean up
+foreach (@versions) {
+ is ((system "pg_dropcluster $_ main --stop"), 0, "pg_dropcluster $_ main");
+}
+check_clean;
+
+# vim: filetype=perl