diff options
author | Daniel Gustafsson <dgustafsson@postgresql.org> | 2023-04-07 22:14:20 +0200 |
---|---|---|
committer | Daniel Gustafsson <dgustafsson@postgresql.org> | 2023-04-07 22:14:20 +0200 |
commit | 664d757531e11ea5ef6971884ddb2a7af6fae69a (patch) | |
tree | 981aa632e9732d6ad1d9c9fca09d8bfaeccb878c /src/test/perl/PostgreSQL/Test/BackgroundPsql.pm | |
parent | 32bc0d022dee250fac9fc787226abed96b8ff894 (diff) | |
download | postgresql-664d757531e11ea5ef6971884ddb2a7af6fae69a.tar.gz postgresql-664d757531e11ea5ef6971884ddb2a7af6fae69a.zip |
Refactor background psql TAP functions
This breaks out the background and interactive psql functionality into a
new class, PostgreSQL::Test::BackgroundPsql. Sessions are still initiated
via PostgreSQL::Test::Cluster, but once started they can be manipulated by
the new helper functions which intend to make querying easier. A sample
session for a command which can be expected to finish at a later time can
be seen below.
my $session = $node->background_psql('postgres');
$bsession->query_until(qr/start/, q(
\echo start
CREATE INDEX CONCURRENTLY idx ON t(a);
));
$bsession->quit;
Patch by Andres Freund with some additional hacking by me.
Author: Andres Freund <andres@anarazel.de>
Reviewed-by: Andrew Dunstan <andrew@dunslane.net>
Discussion: https://postgr.es/m/20230130194350.zj5v467x4jgqt3d6@awork3.anarazel.de
Diffstat (limited to 'src/test/perl/PostgreSQL/Test/BackgroundPsql.pm')
-rw-r--r-- | src/test/perl/PostgreSQL/Test/BackgroundPsql.pm | 299 |
1 files changed, 299 insertions, 0 deletions
diff --git a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm new file mode 100644 index 00000000000..d767e284a29 --- /dev/null +++ b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm @@ -0,0 +1,299 @@ + +# Copyright (c) 2021-2023, PostgreSQL Global Development Group + +=pod + +=head1 NAME + +PostgreSQL::Test::BackgroundPsql - class for controlling background psql processes + +=head1 SYNOPSIS + + use PostgreSQL::Test::Cluster; + + my $node = PostgreSQL::Test::Cluster->new('mynode'); + + # Create a data directory with initdb + $node->init(); + + # Start the PostgreSQL server + $node->start(); + + # Create and start an interactive psql session + my $isession = $node->interactive_psql('postgres'); + # Apply timeout per query rather than per session + $isession->set_query_timer_restart(); + # Run a query and get the output as seen by psql + my $ret = $isession->query("SELECT 1"); + # Run a backslash command and wait until the prompt returns + $isession->query_until(qr/postgres #/, "\\d foo\n"); + # Close the session and exit psql + $isession->quit; + + # Create and start a background psql session + my $bsession = $node->background_psql('postgres'); + + # Run a query which is guaranteed to not return in case it fails + $bsession->query_safe("SELECT 1"); + # Initiate a command which can be expected to terminate at a later stage + $bsession->query_until(qr/start/, q( + \echo start + CREATE INDEX CONCURRENTLY idx ON t(a); + )); + # Close the session and exit psql + $bsession->quit; + +=head1 DESCRIPTION + +PostgreSQL::Test::BackgroundPsql contains functionality for controlling +a background or interactive psql session operating on a PostgreSQL node +initiated by PostgreSQL::Test::Cluster. + +=cut + +package PostgreSQL::Test::BackgroundPsql; + +use strict; +use warnings; + +use Carp; +use Config; +use IPC::Run; +use PostgreSQL::Test::Utils qw(pump_until); +use Test::More; + +=pod + +=head1 METHODS + +=over + +=item PostgreSQL::Test::BackroundPsql->new(interactive, @params) + +Builds a new object of class C<PostgreSQL::Test::BackgroundPsql> for either +an interactive or background session and starts it. If C<interactive> is +true then a PTY will be attached. C<psql_params> should contain the full +command to run psql with all desired parameters and a complete connection +string. For C<interactive> sessions, IO::Pty is required. + +=cut + +sub new +{ + my $class = shift; + my ($interactive, $psql_params) = @_; + my $psql = {'stdin' => '', 'stdout' => '', 'stderr' => '', 'query_timer_restart' => undef}; + my $run; + + # This constructor should only be called from PostgreSQL::Test::Cluster + my ($package, $file, $line) = caller; + die "Forbidden caller of constructor: package: $package, file: $file:$line" + unless $package->isa('PostgreSQL::Test::Cluster'); + + $psql->{timeout} = IPC::Run::timeout($PostgreSQL::Test::Utils::timeout_default); + + if ($interactive) + { + $run = IPC::Run::start $psql_params, + '<pty<', \$psql->{stdin}, '>pty>', \$psql->{stdout}, '2>', \$psql->{stderr}, + $psql->{timeout}; + } + else + { + $run = IPC::Run::start $psql_params, + '<', \$psql->{stdin}, '>', \$psql->{stdout}, '2>', \$psql->{stderr}, + $psql->{timeout}; + } + + $psql->{run} = $run; + + my $self = bless $psql, $class; + + $self->_wait_connect(); + + return $self; +} + +# Internal routine for awaiting psql starting up and being ready to consume +# input. +sub _wait_connect +{ + my ($self) = @_; + + # Request some output, and pump until we see it. This means that psql + # connection failures are caught here, relieving callers of the need to + # handle those. (Right now, we have no particularly good handling for + # errors anyway, but that might be added later.) + my $banner = "background_psql: ready"; + $self->{stdin} .= "\\echo $banner\n"; + $self->{run}->pump() until $self->{stdout} =~ /$banner/ || $self->{timeout}->is_expired; + $self->{stdout} = ''; # clear out banner + + die "psql startup timed out" if $self->{timeout}->is_expired; +} + +=pod + +=item $session->quit + +Close the session and clean up resources. Each test run must be closed with +C<quit>. + +=cut + +sub quit +{ + my ($self) = @_; + + $self->{stdin} .= "\\q\n"; + + return $self->{run}->finish; +} + +=pod + +=item $session->reconnect_and_clear + +Terminate the current session and connect again. + +=cut + +sub reconnect_and_clear +{ + my ($self) = @_; + + # If psql isn't dead already, tell it to quit as \q, when already dead, + # causes IPC::Run to unhelpfully error out with "ack Broken pipe:". + $self->{run}->pump_nb(); + if ($self->{run}->pumpable()) + { + $self->{stdin} .= "\\q\n"; + } + $self->{run}->finish; + + # restart + $self->{run}->run(); + $self->{stdin} = ''; + $self->{stdout} = ''; + + $self->_wait_connect() +} + +=pod + +=item $session->query() + +Executes a query in the current session and returns the output in scalar +context and (output, error) in list context where error is 1 in case there +was output generated on stderr when executing the query. + +=cut + +sub query +{ + my ($self, $query) = @_; + my $ret; + my $output; + local $Test::Builder::Level = $Test::Builder::Level + 1; + + note "issuing query via background psql: $query"; + + $self->{timeout}->start() if (defined($self->{query_timer_restart})); + + # Feed the query to psql's stdin, followed by \n (so psql processes the + # line), by a ; (so that psql issues the query, if it doesnt't include a ; + # itself), and a separator echoed with \echo, that we can wait on. + my $banner = "background_psql: QUERY_SEPARATOR"; + $self->{stdin} .= "$query\n;\n\\echo $banner\n"; + + pump_until($self->{run}, $self->{timeout}, \$self->{stdout}, qr/$banner/); + + die "psql query timed out" if $self->{timeout}->is_expired; + $output = $self->{stdout}; + + # remove banner again, our caller doesn't care + $output =~ s/\n$banner$//s; + + # clear out output for the next query + $self->{stdout} = ''; + + $ret = $self->{stderr} eq "" ? 0 : 1; + + return wantarray ? ( $output, $ret ) : $output; +} + +=pod + +=item $session->query_safe() + +Wrapper around C<query> which errors out if the query failed to execute. +Query failure is determined by it producing output on stderr. + +=cut + +sub query_safe +{ + my ($self, $query) = @_; + + my $ret = $self->query($query); + + if ($self->{stderr} ne "") + { + die "query failed: $self->{stderr}"; + } + + return $ret; +} + +=pod + +=item $session->query_until(until, query) + +Issue C<query> and wait for C<until> appearing in the query output rather than +waiting for query completion. C<query> needs to end with newline and semicolon +(if applicable, interactive psql input may not require it) for psql to process +the input. + +=cut + +sub query_until +{ + my ($self, $until, $query) = @_; + my $ret; + local $Test::Builder::Level = $Test::Builder::Level + 1; + + $self->{timeout}->start() if (defined($self->{query_timer_restart})); + $self->{stdin} .= $query; + + pump_until($self->{run}, $self->{timeout}, \$self->{stdout}, $until); + + die "psql query timed out" if $self->{timeout}->is_expired; + + $ret = $self->{stdout}; + + # clear out output for the next query + $self->{stdout} = ''; + + return $ret; +} + +=pod + +=item $session->set_query_timer_restart() + +Configures the timer to be restarted before each query such that the defined +timeout is valid per query rather than per test run. + +=back + +=cut + +sub set_query_timer_restart +{ + my $self = shift; + + $self->{query_timer_restart} = shift if @_; + return $self->{query_timer_restart}; +} + +1; |