From 0271314446293a61457f0b8e6a77d784804bae43 Mon Sep 17 00:00:00 2001 From: Craig Ringer Date: Mon, 22 Feb 2016 18:54:24 +0800 Subject: [PATCH] Add README and example to document how to create TAP tests --- src/test/Makefile | 2 +- src/test/README | 37 ++++ src/test/example_suite/.gitignore | 1 + src/test/example_suite/Makefile | 9 + src/test/example_suite/t/001_minimal.pl | 157 ++++++++++++++ src/test/mb/.gitignore | 1 + src/test/modules/README | 10 + src/test/perl/PostgresNode.pm | 374 +++++++++++++++++++++++++++++--- src/test/perl/README | 37 ++++ 9 files changed, 596 insertions(+), 32 deletions(-) create mode 100644 src/test/README create mode 100644 src/test/example_suite/.gitignore create mode 100644 src/test/example_suite/Makefile create mode 100644 src/test/example_suite/t/001_minimal.pl create mode 100644 src/test/mb/.gitignore create mode 100644 src/test/modules/README create mode 100644 src/test/perl/README diff --git a/src/test/Makefile b/src/test/Makefile index b713c2c..e9b0136 100644 --- a/src/test/Makefile +++ b/src/test/Makefile @@ -12,7 +12,7 @@ subdir = src/test top_builddir = ../.. include $(top_builddir)/src/Makefile.global -SUBDIRS = regress isolation modules +SUBDIRS = regress isolation modules example_suite # We don't build or execute examples/, locale/, or thread/ by default, # but we do want "make clean" etc to recurse into them. Likewise for ssl/, diff --git a/src/test/README b/src/test/README new file mode 100644 index 0000000..485f20b --- /dev/null +++ b/src/test/README @@ -0,0 +1,37 @@ +This directory contains a variety of test infrastructure as well as some of the +tests in PostgreSQL. Not all tests are here - in particular, there are more in +individual contrib/ modules and in src/bin. + +Not all these tests get run by "make check". Check src/test/Makefile to see +which tests get run automatically. + +examples/ + demo programs for libpq that double as regression tests via "make check" + +isolation/ + tests for concurrent behaviours at the SQL level + +locale/ + sanity checks for locale data, encodings, etc + +mb/ + tests for multibyte encoding (utf-8) support + +modules/ + extensions used only or mainly for test purposes, generally not useful + or suitable for installing in production databases. Some of these have + their own tests, some are used by tests elsewhere. + +perl/ + infrastructure for Perl-based Test::More tests. There are no actual tests + here, the code is used by other tests in src/bin/, contrib/ and in the + subdirectories of src/test. + +regress/ + PostgreSQL's main regression test suite, pg_regress + +ssl/ + Tests to exercise and verify SSL certificate handling + +thread/ + A thread-safety-testing utility used by configure, see its README diff --git a/src/test/example_suite/.gitignore b/src/test/example_suite/.gitignore new file mode 100644 index 0000000..b6a2a01 --- /dev/null +++ b/src/test/example_suite/.gitignore @@ -0,0 +1 @@ +/tmp_check/ diff --git a/src/test/example_suite/Makefile b/src/test/example_suite/Makefile new file mode 100644 index 0000000..f557c7d --- /dev/null +++ b/src/test/example_suite/Makefile @@ -0,0 +1,9 @@ +subdir = src/test/example_suite +top_builddir = ../../.. +include $(top_builddir)/src/Makefile.global + +check: + $(prove_check) + +clean: + rm -rf ./tmp_check diff --git a/src/test/example_suite/t/001_minimal.pl b/src/test/example_suite/t/001_minimal.pl new file mode 100644 index 0000000..7fe489a --- /dev/null +++ b/src/test/example_suite/t/001_minimal.pl @@ -0,0 +1,157 @@ +=pod + +=head1 Demo tests + +This is a minimal example showing how to use the Perl-based test framework for +writing tests that are more complicated than pg_regress or the isolation tester +can handle. + +Test scripts should all begin with: + + use strict; + use warnings; + use 5.8.8; + use PostgresNode; + use TestLib; + +The script must include: + + use Test::More tests => 1; + +where the number of tests expected to run is the value of 'tests'. + +All tests must support running under Perl 5.8.8. "use 5_008_008" does NOT +ensure that. + +=cut + +use strict; +use warnings; +use 5.8.8; +use PostgresNode; +use TestLib; +use Test::More tests => 5; + +my $verbose = 0; + +=pod + +=head2 Set up a node + +Unlike pg_regress tests, Perl-based tests must create their own PostgreSQL +node (s). The nodes are managed with the PostgresNode module; see that module +for methods available for the node. + +=cut + +diag 'Setting up node "master"' if $verbose; +my $node = get_new_node('master'); +$node->init; +$node->start; + +diag $node->info() if $verbose; + +=pod + +=head2 Trivial SELECT test + +A very simple test that just runs a SELECT and checks the result. + +Obviously you should write tests like this with pg_regress, but this +serves to demonstrate the minimal setup required. + +=cut + +my $ret = $node->psql('postgres', 'SELECT 1;'); +is($ret, '1', 'simple SELECT'); + +=pod + +=head2 Create a read-replica + +One of the reasons to write Perl tests is to do work that touches +multiple nodes, so lets set up a second node. + +=cut + +diag 'Reconfiguring master for replication' if $verbose; +$node->append_conf('postgresql.conf', "max_wal_senders = 2\n"); +$node->append_conf('postgresql.conf', "wal_level = logical\n"); +$node->append_conf('postgresql.conf', "hot_standby = on\n"); +$node->restart('fast'); + +diag 'Making base backup of current node' if $verbose; +$ret = $node->backup('makereplica', + write_recovery_conf => 1, + xlog_stream => 1); +ok($ret, 'base backup of master'); + +diag 'Setting up node "replica"' if $verbose; +my $replica = get_new_node('replica'); +$replica->init_from_backup($node, 'makereplica'); +$replica->start; + +diag $replica->info() if $verbose; + +=pod + +=head2 Wait for replica to catch up + +A simple example of the sort of tests that can be written with this +toolset: write to a master server then wait until the replica server +replays the change. + +=cut + +diag 'Setting up for catchup tests' if $verbose; + +$node->psql('postgres', q| + CREATE TABLE replica_test(blah text); + INSERT INTO replica_test(blah) VALUES ('fruit bat'); + |); + +# method one: poll until the new row becomes visible on the replica +diag 'Waiting for replica to catch up - polling' if $verbose; +$ret = $replica->poll_query_until('postgres', + q|SELECT 't' FROM replica_test WHERE blah = 'fruit bat';|); + +ok($ret, 'caught up - polling for row'); + +# Method two: wait for the replica replay position to overtake the master +# by querying the replica. +$node->psql('postgres', q|INSERT INTO replica_test(blah) VALUES ('donkey');|); + +my $lsn = $node->psql('postgres', 'SELECT pg_current_xlog_location()'); + +diag "Waiting for replica to catch up to $lsn - replica xlog location" if $verbose; +$ret = $replica->poll_query_until('postgres', + qq|SELECT pg_xlog_location_diff('$lsn', pg_last_xlog_replay_location()) <= 0;|); + +ok($ret, 'caught up - polling for replay on replica'); + +# Method three: wait for the replica to catch up according to +# pg_stat_replication on the master. +$node->psql('postgres', q|INSERT INTO replica_test(blah) VALUES ('turkey');|); + +$lsn = $node->psql('postgres', 'SELECT pg_current_xlog_location()'); + +# This assumes only one replica... +diag "Waiting for replica to catch up to $lsn - pg_stat_replication" if $verbose; +$ret = $node->poll_query_until('postgres', + qq|SELECT pg_xlog_location_diff('$lsn', replay_location) <= 0 + FROM pg_stat_replication|); + +ok($ret, 'caught up - polling pg_stat_replication on master'); + +=pod + +=head2 Test teardown + +Every test should end by explicitly tearing down the node (s) it created. + +=cut + +$node->teardown_node; +$replica->teardown_node; + +1; diff --git a/src/test/mb/.gitignore b/src/test/mb/.gitignore new file mode 100644 index 0000000..6628455 --- /dev/null +++ b/src/test/mb/.gitignore @@ -0,0 +1 @@ +/results/ diff --git a/src/test/modules/README b/src/test/modules/README new file mode 100644 index 0000000..290268d --- /dev/null +++ b/src/test/modules/README @@ -0,0 +1,10 @@ +src/test/modules contains PostgreSQL extensions that are primarily or entirely +intended for testing PostgreSQL and/or to serve as example code. The extensions +here aren't intended to be installed in a production server and aren't useful +for "real work". + +Most extensions have their own pg_regress tests. Some are also used by tests +elsewhere in the test tree. + +If you're adding new hooks or other functionality exposed as C-level API this +is where to add the tests for it. diff --git a/src/test/perl/PostgresNode.pm b/src/test/perl/PostgresNode.pm index 2ab9aee..08109a7 100644 --- a/src/test/perl/PostgresNode.pm +++ b/src/test/perl/PostgresNode.pm @@ -1,8 +1,15 @@ -# PostgresNode, class representing a data directory and postmaster. -# -# This contains a basic set of routines able to work on a PostgreSQL node, -# allowing to start, stop, backup and initialize it with various options. -# The set of nodes managed by a given test is also managed by this module. +=pod + +=head1 PostgresNode - class representing a data directory and postmaster + +This contains a basic set of routines able to work on a PostgreSQL node, +allowing to start, stop, backup and initialize it with various options. +The set of nodes managed by a given test is also managed by this module. + +It requires IPC::Run - install libperl-ipc-run (Debian/Ubuntu) or perl-IPC-Run +(Fedora/RHEL). + +=cut package PostgresNode; @@ -40,6 +47,21 @@ INIT $last_port_assigned = int(rand() * 16384) + 49152; } +=pod + +=head2 Methods + +=over + +=item PostgresNode::new($class, $name, $pghost, $pgport) + +Create a new PostgresNode instance. Does not initdb or start it. + +You should generally prefer to use get_new_node() instead since it takes care +of finding port numbers, registering instances for cleanup, etc. + +=cut + sub new { my $class = shift; @@ -61,36 +83,91 @@ sub new return $self; } +=pod + +=item $node->port() + +Get the port number assigned to the host. This won't necessarily be a TCP port +open on the local host since we prefer to use unix sockets if possible. + +Use $node->connstr() if you want a connection string. + +=cut + sub port { my ($self) = @_; return $self->{_port}; } +=pod + +=item $node->host() + +Return the host (like PGHOST) for this instance. May be a UNIX socket path. + +Use $node->connstr() if you want a connection string. + +=cut + sub host { my ($self) = @_; return $self->{_host}; } +=pod + +=item $node->basedir() + +The directory all the node's files will be within - datadir, archive directory, +backups, etc. + +=cut + sub basedir { my ($self) = @_; return $self->{_basedir}; } +=pod + +=item $node->name() + +The name assigned to the node at creation time. + +=cut + sub name { my ($self) = @_; return $self->{_name}; } +=pod + +=item $node->logfile() + +Path to the PostgreSQL log file for this instance. + +=cut + sub logfile { my ($self) = @_; return $self->{_logfile}; } +=pod + +=item $node->connstr() + +Get a libpq connection string that will establish a connection to +this node. Suitable for passing to psql, DBD::Pg, etc. + +=cut + sub connstr { my ($self, $dbname) = @_; @@ -103,6 +180,15 @@ sub connstr return "port=$pgport host=$pghost dbname=$dbname"; } +=pod + +=item $node->data_dir() + +Returns the path to the data directory. postgresql.conf and pg_hba.conf are +always here. + +=cut + sub data_dir { my ($self) = @_; @@ -110,6 +196,14 @@ sub data_dir return "$res/pgdata"; } +=pod + +=item $node->archive_dir() + +If archiving is enabled, WAL files go here. + +=cut + sub archive_dir { my ($self) = @_; @@ -117,6 +211,14 @@ sub archive_dir return "$basedir/archives"; } +=pod + +=item $node->backup_dir() + +The output path for backups taken with $node->backup() + +=cut + sub backup_dir { my ($self) = @_; @@ -124,23 +226,55 @@ sub backup_dir return "$basedir/backup"; } -# Dump node information +=pod + +=item $node->info() + +Return a string containing human-readable diagnostic information (paths, etc) +about this node. + +=cut + +sub info +{ + my ($self) = @_; + my $_info = ''; + open my $fh, '>', \$_info or die; + print $fh "Name: " . $self->name . "\n"; + print $fh "Data directory: " . $self->data_dir . "\n"; + print $fh "Backup directory: " . $self->backup_dir . "\n"; + print $fh "Archive directory: " . $self->archive_dir . "\n"; + print $fh "Connection string: " . $self->connstr . "\n"; + print $fh "Log file: " . $self->logfile . "\n"; + close $fh or die; + return $_info; +} + +=pod + +=item $node->dump_info() + +Print $node->info() + +=cut + sub dump_info { my ($self) = @_; - print "Name: " . $self->name . "\n"; - print "Data directory: " . $self->data_dir . "\n"; - print "Backup directory: " . $self->backup_dir . "\n"; - print "Archive directory: " . $self->archive_dir . "\n"; - print "Connection string: " . $self->connstr . "\n"; - print "Log file: " . $self->logfile . "\n"; + print $self->info; } + +# Internal method to set up trusted pg_hba.conf for replication. Not +# documented because you shouldn't use it, it's called automatically if needed. sub set_replication_conf { my ($self) = @_; my $pgdata = $self->data_dir; + $self->host eq $test_pghost + or die "set_replication_conf only works with the default host"; + open my $hba, ">>$pgdata/pg_hba.conf"; print $hba "\n# Allow replication (set up by PostgresNode.pm)\n"; if (!$TestLib::windows_os) @@ -155,13 +289,26 @@ sub set_replication_conf close $hba; } -# Initialize a new cluster for testing. -# -# Authentication is set up so that only the current OS user can access the -# cluster. On Unix, we use Unix domain socket connections, with the socket in -# a directory that's only accessible to the current user to ensure that. -# On Windows, we use SSPI authentication to ensure the same (by pg_regress -# --config-auth). +=pod + +=item $node->init(...) + +Initialize a new cluster for testing. + +Authentication is set up so that only the current OS user can access the +cluster. On Unix, we use Unix domain socket connections, with the socket in +a directory that's only accessible to the current user to ensure that. +On Windows, we use SSPI authentication to ensure the same (by pg_regress +--config-auth). + +pg_hba.conf is configured to allow replication connections. Pass the keyword +parameter hba_permit_replication => 0 to disable this. + +The new node is set up in a fast but unsafe configuration where fsync is +disabled. + +=cut + sub init { my ($self, %params) = @_; @@ -197,6 +344,19 @@ sub init $self->set_replication_conf if ($params{hba_permit_replication}); } +=pod + +=item $node->append_conf(filename, str) + +A shortcut method to append to files like pg_hba.conf and postgresql.conf. + +Does no validation or sanity checking. Does not reload the configuration +after writing. + +A newline is NOT automatically appended to the string. + +=cut + sub append_conf { my ($self, $filename, $str) = @_; @@ -206,18 +366,66 @@ sub append_conf TestLib::append_to_file($conffile, $str); } +=pod + +=item $node->backup(backup_name, ...) + +Create a hot backup with pg_basebackup in $node->backup_dir, +including the transaction logs. xlogs are fetched at the +end of the backup, not streamed. + +You'll have to configure a suitable max_wal_senders on the +target server since it isn't done by default. + +Additional options may be passed as a hash: + +=over + +=item write_recovery_conf => 1 + +use pg_basebackup -R to write a recovery.conf + +=item xlog_stream => 1 + +Stream WAL archives during backup instead of copying them at the end. + +=back + +=cut + sub backup { - my ($self, $backup_name) = @_; + my ($self, $backup_name, %params) = @_; my $backup_path = $self->backup_dir . '/' . $backup_name; my $port = $self->port; my $name = $self->name; + my $recovery_opt = ''; + $recovery_opt = '-R' if ($params{write_recovery_conf}); + + my $xlog_opt = '-x'; + $xlog_opt = '-X stream' if ($params{xlog_stream}); + print "# Taking backup $backup_name from node \"$name\"\n"; - TestLib::system_or_bail("pg_basebackup -D $backup_path -p $port -x"); + TestLib::system_or_bail("pg_basebackup -D $backup_path -p $port $xlog_opt $recovery_opt"); print "# Backup finished\n"; } +=pod + +=item $node->init_from_backup(root_node, backup_name) + +Initialize a node from a backup, which may come from this node or a different +node. root_node must be a PostgresNode reference, backup_name the string name +of a backup previously created on that node with $node->backup. + +Does not start the node after init. + +The backup is copied, leaving the original unmodified. pg_hba.conf is +unconditionally set to enable replication connections. + +=cut + sub init_from_backup { my ($self, $root_node, $backup_name) = @_; @@ -248,6 +456,16 @@ port = $port $self->set_replication_conf; } +=pod + +=item $node->start() + +Wrapper for pg_ctl -w start + +Start the node and wait until it is ready to accept connections. + +=cut + sub start { my ($self) = @_; @@ -268,6 +486,14 @@ sub start $self->_update_pid; } +=pod + +=item $node->stop(mode) + +Stop the node using pg_ctl -m $mode and wait for it to stop. + +=cut + sub stop { my ($self, $mode) = @_; @@ -281,6 +507,14 @@ sub stop $self->_update_pid; } +=pod + +=item $node->restart() + +wrapper for pg_ctl -w restart + +=cut + sub restart { my ($self) = @_; @@ -294,6 +528,8 @@ sub restart $self->_update_pid; } + +# Internal method sub _update_pid { my $self = shift; @@ -314,14 +550,20 @@ sub _update_pid print "# No postmaster PID\n"; } -# -# Cluster management functions -# +=pod + +=item get_new_node(node_name) + +Build a new PostgresNode object, assigning a free port number. Standalone +function that's automatically imported. + +We also register the node, to avoid the port number from being reused +for another node even when this one is not active. + +You should generally use this instead of PostgresNode::new(...). + +=cut -# Build a new PostgresNode object, assigning a free port number. -# -# We also register the node, to avoid the port number from being reused -# for another node even when this one is not active. sub get_new_node { my $name = shift; @@ -360,6 +602,7 @@ sub get_new_node return $node; } +# Attempt automatic cleanup sub DESTROY { my $self = shift; @@ -369,6 +612,14 @@ sub DESTROY TestLib::system_log('pg_ctl', 'kill', 'QUIT', $self->{_pid}); } +=pod + +=item $node->teardown_node() + +Do an immediate stop of the node + +=cut + sub teardown_node { my $self = shift; @@ -376,6 +627,18 @@ sub teardown_node $self->stop('immediate'); } +=pod + +=item $node->psql(dbname, sql) + +Run a query with psql and return stdout, or on error print stderr. + +Executes a query/script with psql and returns psql's standard output. psql is +run in unaligned tuples-only quiet mode with psqlrc disabled so simple queries +will just return the result row(s) with fields separated by commas. + +=cut + sub psql { my ($self, $dbname, $sql) = @_; @@ -399,7 +662,15 @@ sub psql return $stdout; } -# Run a query once a second, until it returns 't' (i.e. SQL boolean true). +=pod + +=item $node->poll_query_until(dbname, query) + +Run a query once a second, until it returns 't' (i.e. SQL boolean true). +Continues polling if psql returns an error result. Times out after 90 seconds. + +=cut + sub poll_query_until { my ($self, $dbname, $query) = @_; @@ -432,6 +703,16 @@ sub poll_query_until return 0; } +=pod + +=item $node->command_ok(...) + +Runs a shell command like TestLib::command_ok, but with PGPORT +set so that the command will default to connecting to this +PostgresNode. + +=cut + sub command_ok { my $self = shift; @@ -441,6 +722,14 @@ sub command_ok TestLib::command_ok(@_); } +=pod + +=item $node->command_fails(...) - TestLib::command_fails with our PGPORT + +See command_ok(...) + +=cut + sub command_fails { my $self = shift; @@ -450,6 +739,14 @@ sub command_fails TestLib::command_fails(@_); } +=pod + +=item $node->command_like(...) + +TestLib::command_like with our PGPORT. See command_ok(...) + +=cut + sub command_like { my $self = shift; @@ -459,8 +756,17 @@ sub command_like TestLib::command_like(@_); } -# Run a command on the node, then verify that $expected_sql appears in the -# server log file. +=pod + +=item $node->issues_sql_like(cmd, expected_sql, test_name) + +Run a command on the node, then verify that $expected_sql appears in the +server log file. + +Reads the whole log file so be careful when working with large log outputs. + +=cut + sub issues_sql_like { my ($self, $cmd, $expected_sql, $test_name) = @_; @@ -474,4 +780,10 @@ sub issues_sql_like like($log, $expected_sql, "$test_name: SQL found in server log"); } +=pod + +=back + +=cut + 1; diff --git a/src/test/perl/README b/src/test/perl/README new file mode 100644 index 0000000..d635a18 --- /dev/null +++ b/src/test/perl/README @@ -0,0 +1,37 @@ +Perl-based (Test::More) tests +--- + +src/test/perl/ contains shared infrastructure that's used by Perl-based tests +across the source tree, particularly tests in src/bin and src/test. It's used +to drive tests for backup and restore, replication, etc - anything that can't +really be expressed using pg_regress or the isolation test framework. + +You should prefer to write tests using pg_regress in src/test/regress, or +isolation tester specs in src/test/isolation, if possible. If not, check to see +if your new tests make sense under an existing tree in src/test, like +src/test/ssl, or should be added to one of the suites for an existing utility + +Writing tests +=== + +Tests are written using Perl's Test::More with some PostgreSQL-specific +infrastructure from src/test/perl providing node management, support for +invoking 'psql' to run queries and get results, etc. You should read the +documentation for Test::More before trying to write tests. + +src/test/sample_suite/t contains some sample tests that describe the structure +of tests, test compatibility requirements, etc. There are tests there with +sample usage of the node management support. Start with + + perldoc src/test/example_suite/t/001_minimal.pl + +Adding a suite +=== + +If your tests don't make sense under any of the existing test suites you can +add a new suite in src/test/ . There's an example suite in src/test/sample_suite +that can be copied and modified to suit. Try to avoid creating lots of new +test suites - it's better to add new tests to an existing suite if they can +reasonably fit there. + +You should add the new suite to src/test/Makefile . See the comments there. -- 2.1.0