From fb0e9fceead01aaee0bdd05c3ad6c67814f6b820 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Mon, 30 Jan 2023 15:39:08 -0800
Subject: [PATCH v1] WIP: test: Introduce BackgroundPsql class

Discussion: https://postgr.es/m/882122.1675109206@sss.pgh.pa.us
---
 src/bin/psql/t/010_tab_completion.pl          |  27 +--
 contrib/amcheck/t/003_cic_2pc.pl              |  66 +++----
 .../perl/PostgreSQL/Test/BackgroundPsql.pm    | 165 ++++++++++++++++++
 src/test/perl/PostgreSQL/Test/Cluster.pm      |  83 ++-------
 src/test/recovery/t/031_recovery_conflict.pl  | 101 +++--------
 src/test/subscription/t/015_stream.pl         |  52 ++----
 6 files changed, 252 insertions(+), 242 deletions(-)
 create mode 100644 src/test/perl/PostgreSQL/Test/BackgroundPsql.pm

diff --git a/src/bin/psql/t/010_tab_completion.pl b/src/bin/psql/t/010_tab_completion.pl
index 7746c75e0c9..7c382b98f8f 100644
--- a/src/bin/psql/t/010_tab_completion.pl
+++ b/src/bin/psql/t/010_tab_completion.pl
@@ -92,14 +92,7 @@ print $FH "other stuff\n";
 close $FH;
 
 # fire up an interactive psql session
-my $in  = '';
-my $out = '';
-
-my $timer = timer($PostgreSQL::Test::Utils::timeout_default);
-
-my $h = $node->interactive_psql('postgres', \$in, \$out, $timer);
-
-like($out, qr/psql/, "print startup banner");
+my $h = $node->interactive_psql('postgres');
 
 # Simple test case: type something and see if psql responds as expected
 sub check_completion
@@ -109,15 +102,12 @@ sub check_completion
 	# report test failures from caller location
 	local $Test::Builder::Level = $Test::Builder::Level + 1;
 
-	# reset output collector
-	$out = "";
 	# restart per-command timer
-	$timer->start($PostgreSQL::Test::Utils::timeout_default);
-	# send the data to be sent
-	$in .= $send;
-	# wait ...
-	pump $h until ($out =~ $pattern || $timer->is_expired);
-	my $okay = ($out =~ $pattern && !$timer->is_expired);
+	$h->{timeout}->start($PostgreSQL::Test::Utils::timeout_default);
+
+	# send the data to be sent and wait for its result
+	my $out = $h->query_until($pattern, $send);
+	my $okay = ($out =~ $pattern && !$h->{timeout}->is_expired);
 	ok($okay, $annotation);
 	# for debugging, log actual output if it didn't match
 	local $Data::Dumper::Terse = 1;
@@ -443,10 +433,7 @@ check_completion("blarg \t\t", qr//, "check completion failure path");
 clear_query();
 
 # send psql an explicit \q to shut it down, else pty won't close properly
-$timer->start($PostgreSQL::Test::Utils::timeout_default);
-$in .= "\\q\n";
-finish $h or die "psql returned $?";
-$timer->reset;
+$h->quit or die "psql returned $?";
 
 # done
 $node->stop;
diff --git a/contrib/amcheck/t/003_cic_2pc.pl b/contrib/amcheck/t/003_cic_2pc.pl
index eabe6fcf964..5323ed11ae9 100644
--- a/contrib/amcheck/t/003_cic_2pc.pl
+++ b/contrib/amcheck/t/003_cic_2pc.pl
@@ -36,63 +36,46 @@ $node->safe_psql('postgres', q(CREATE TABLE tbl(i int)));
 # statements.
 #
 
-my $main_in    = '';
-my $main_out   = '';
-my $main_timer = IPC::Run::timeout($PostgreSQL::Test::Utils::timeout_default);
+my $main_h = $node->background_psql('postgres');
 
-my $main_h =
-  $node->background_psql('postgres', \$main_in, \$main_out,
-	$main_timer, on_error_stop => 1);
-$main_in .= q(
+$main_h->query_safe(q(
 BEGIN;
 INSERT INTO tbl VALUES(0);
-\echo syncpoint1
-);
-pump $main_h until $main_out =~ /syncpoint1/ || $main_timer->is_expired;
+));
 
-my $cic_in    = '';
-my $cic_out   = '';
-my $cic_timer = IPC::Run::timeout($PostgreSQL::Test::Utils::timeout_default);
-my $cic_h =
-  $node->background_psql('postgres', \$cic_in, \$cic_out,
-	$cic_timer, on_error_stop => 1);
-$cic_in .= q(
+my $cic_h = $node->background_psql('postgres');
+
+$cic_h->query_until(qr/start/, q(
 \echo start
 CREATE INDEX CONCURRENTLY idx ON tbl(i);
-);
-pump $cic_h until $cic_out =~ /start/ || $cic_timer->is_expired;
+));
 
-$main_in .= q(
+$main_h->query_safe(q(
 PREPARE TRANSACTION 'a';
-);
+));
 
-$main_in .= q(
+$main_h->query_safe(q(
 BEGIN;
 INSERT INTO tbl VALUES(0);
-\echo syncpoint2
-);
-pump $main_h until $main_out =~ /syncpoint2/ || $main_timer->is_expired;
+));
 
 $node->safe_psql('postgres', q(COMMIT PREPARED 'a';));
 
-$main_in .= q(
+$main_h->query_safe(q(
 PREPARE TRANSACTION 'b';
 BEGIN;
 INSERT INTO tbl VALUES(0);
-\echo syncpoint3
-);
-pump $main_h until $main_out =~ /syncpoint3/ || $main_timer->is_expired;
+));
 
 $node->safe_psql('postgres', q(COMMIT PREPARED 'b';));
 
-$main_in .= q(
+$main_h->query_safe(q(
 PREPARE TRANSACTION 'c';
 COMMIT PREPARED 'c';
-);
-$main_h->pump_nb;
+));
 
-$main_h->finish;
-$cic_h->finish;
+$main_h->quit;
+$cic_h->quit;
 
 $result = $node->psql('postgres', q(SELECT bt_index_check('idx',true)));
 is($result, '0', 'bt_index_check after overlapping 2PC');
@@ -113,22 +96,15 @@ PREPARE TRANSACTION 'persists_forever';
 ));
 $node->restart;
 
-my $reindex_in  = '';
-my $reindex_out = '';
-my $reindex_timer =
-  IPC::Run::timeout($PostgreSQL::Test::Utils::timeout_default);
-my $reindex_h =
-  $node->background_psql('postgres', \$reindex_in, \$reindex_out,
-	$reindex_timer, on_error_stop => 1);
-$reindex_in .= q(
+my $reindex_h = $node->background_psql('postgres');
+$reindex_h->query_until(qr/start/, q(
 \echo start
 DROP INDEX CONCURRENTLY idx;
 CREATE INDEX CONCURRENTLY idx ON tbl(i);
-);
-pump $reindex_h until $reindex_out =~ /start/ || $reindex_timer->is_expired;
+));
 
 $node->safe_psql('postgres', "COMMIT PREPARED 'spans_restart'");
-$reindex_h->finish;
+$reindex_h->quit;
 $result = $node->psql('postgres', q(SELECT bt_index_check('idx',true)));
 is($result, '0', 'bt_index_check after 2PC and restart');
 
diff --git a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
new file mode 100644
index 00000000000..9257075224d
--- /dev/null
+++ b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
@@ -0,0 +1,165 @@
+
+# Copyright (c) 2021-2023, PostgreSQL Global Development Group
+
+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;
+
+# Start a new psql background session
+#
+# Parameters:
+# - "interactive" - should a PTY be used
+# - "psql" - psql command, parameters, including connection string
+sub new
+{
+	my $class = shift;
+	my ($interactive, $psql_params) = @_;
+	my $psql = {'stdin' => '', 'stdout' => '', 'stderr' => ''};
+	my $run;
+
+	$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;
+}
+
+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;
+}
+
+sub quit
+{
+	my ($self) = @_;
+
+	$self->{stdin} .= "\\q\n";
+
+	return $self->{run}->finish;
+}
+
+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()
+}
+
+sub query
+{
+	my ($self, $query) = @_;
+	my $ret;
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	note "issuing query via background psql: $query";
+
+	# feed the query to psql's stdin, follwed 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;
+	$ret = $self->{stdout};
+
+	# remove banner again, our caller doesn't care
+	$ret =~ s/\n$banner$//s;
+
+	# clear out output for the next query
+	$self->{stdout} = '';
+
+	return $ret;
+}
+
+# Like query(), but errors out if the query failed.
+#
+# Query failure is determined by producing output on stderr.
+sub query_safe
+{
+	my ($self, $query) = @_;
+
+	my $ret = $self->query($query);
+
+	if ($self->{stderr} ne "")
+	{
+		die "query failed: $self->{stderr}";
+	}
+
+	return $ret;
+}
+
+# issue query, but only wait for $until, not query completion
+#
+# Note: Query needs newline and semicolon for psql to process the input.
+sub query_until
+{
+	my ($self, $until, $query) = @_;
+	my $ret;
+	local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+	$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;
+}
+
+1;
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 04921ca3a3d..e16ff197cf9 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -113,6 +113,7 @@ use PostgreSQL::Test::RecursiveCopy;
 use Socket;
 use Test::More;
 use PostgreSQL::Test::Utils ();
+use PostgreSQL::Test::BackgroundPsql ();
 use Time::HiRes qw(usleep);
 use Scalar::Util qw(blessed);
 
@@ -1966,18 +1967,12 @@ sub psql
 
 =pod
 
-=item $node->background_psql($dbname, \$stdin, \$stdout, $timer, %params) => harness
+=item $node->background_psql($dbname, %params) => BackgroundPsql instance
 
-Invoke B<psql> on B<$dbname> and return an IPC::Run harness object, which the
-caller may use to send input to B<psql>.  The process's stdin is sourced from
-the $stdin scalar reference, and its stdout and stderr go to the $stdout
-scalar reference.  This allows the caller to act on other parts of the system
-while idling this backend.
+Invoke B<psql> on B<$dbname> and return a BackgroundPsql object.
 
-The specified timer object is attached to the harness, as well.  It's caller's
-responsibility to set the timeout length (usually
-$PostgreSQL::Test::Utils::timeout_default), and to restart the timer after
-each command if the timeout is per-command.
+A default timeout of $PostgreSQL::Test::Utils::timeout_default is set up,
+which can modified later.
 
 psql is invoked in tuples-only unaligned mode with reading of B<.psqlrc>
 disabled.  That may be overridden by passing extra psql parameters.
@@ -1986,7 +1981,7 @@ Dies on failure to invoke psql, or if psql fails to connect.  Errors occurring
 later are the caller's problem.  psql runs with on_error_stop by default so
 that it will stop running sql and return 3 if passed SQL results in an error.
 
-Be sure to "finish" the harness when done with it.
+Be sure to "quit" the returned object when done with it.
 
 =over
 
@@ -2012,7 +2007,7 @@ If given, it must be an array reference containing additional parameters to B<ps
 
 sub background_psql
 {
-	my ($self, $dbname, $stdin, $stdout, $timer, %params) = @_;
+	my ($self, $dbname, %params) = @_;
 
 	local %ENV = $self->_get_env();
 
@@ -2029,45 +2024,18 @@ sub background_psql
 
 	$params{on_error_stop} = 1 unless defined $params{on_error_stop};
 
-	push @psql_params, '-v', 'ON_ERROR_STOP=1' if $params{on_error_stop};
-	push @psql_params, @{ $params{extra_params} }
-	  if defined $params{extra_params};
-
-	# Ensure there is no data waiting to be sent:
-	$$stdin = "" if ref($stdin);
-	# IPC::Run would otherwise append to existing contents:
-	$$stdout = "" if ref($stdout);
-
-	my $harness = IPC::Run::start \@psql_params,
-	  '<', $stdin, '>', $stdout, $timer;
-
-	# 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";
-	$$stdin = "\\echo $banner\n";
-	pump $harness until $$stdout =~ /$banner/ || $timer->is_expired;
-
-	die "psql startup timed out" if $timer->is_expired;
-
-	return $harness;
+	return PostgreSQL::Test::BackgroundPsql->new(0, \@psql_params);
 }
 
 =pod
 
-=item $node->interactive_psql($dbname, \$stdin, \$stdout, $timer, %params) => harness
+=item $node->interactive_psql($dbname, %params) => BackgroundPsql instance
 
-Invoke B<psql> on B<$dbname> and return an IPC::Run harness object,
-which the caller may use to send interactive input to B<psql>.
-The process's stdin is sourced from the $stdin scalar reference,
-and its stdout and stderr go to the $stdout scalar reference.
-ptys are used so that psql thinks it's being called interactively.
+Invoke B<psql> on B<$dbname> and return a BackgroundPsql object, which the
+caller may use to send interactive input to B<psql>.
 
-The specified timer object is attached to the harness, as well.  It's caller's
-responsibility to set the timeout length (usually
-$PostgreSQL::Test::Utils::timeout_default), and to restart the timer after
-each command if the timeout is per-command.
+A default timeout of $PostgreSQL::Test::Utils::timeout_default is set up,
+which can modified later.
 
 psql is invoked in tuples-only unaligned mode with reading of B<.psqlrc>
 disabled.  That may be overridden by passing extra psql parameters.
@@ -2075,7 +2043,7 @@ disabled.  That may be overridden by passing extra psql parameters.
 Dies on failure to invoke psql, or if psql fails to connect.
 Errors occurring later are the caller's problem.
 
-Be sure to "finish" the harness when done with it.
+Be sure to "quit" the returned object when done with it.
 
 The only extra parameter currently accepted is
 
@@ -2093,7 +2061,7 @@ This requires IO::Pty in addition to IPC::Run.
 
 sub interactive_psql
 {
-	my ($self, $dbname, $stdin, $stdout, $timer, %params) = @_;
+	my ($self, $dbname, %params) = @_;
 
 	local %ENV = $self->_get_env();
 
@@ -2104,26 +2072,7 @@ sub interactive_psql
 	push @psql_params, @{ $params{extra_params} }
 	  if defined $params{extra_params};
 
-	# Ensure there is no data waiting to be sent:
-	$$stdin = "" if ref($stdin);
-	# IPC::Run would otherwise append to existing contents:
-	$$stdout = "" if ref($stdout);
-
-	my $harness = IPC::Run::start \@psql_params,
-	  '<pty<', $stdin, '>pty>', $stdout, $timer;
-
-	# Pump until we see psql's help banner.  This ensures that callers
-	# won't write anything to the pty before it's ready, avoiding an
-	# implementation issue in IPC::Run.  Also, it 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.)
-	pump $harness
-	  until $$stdout =~ /Type "help" for help/ || $timer->is_expired;
-
-	die "psql startup timed out" if $timer->is_expired;
-
-	return $harness;
+	return PostgreSQL::Test::BackgroundPsql->new(1, \@psql_params);
 }
 
 # Common sub of pgbench-invoking interfaces.  Makes any requested script files
diff --git a/src/test/recovery/t/031_recovery_conflict.pl b/src/test/recovery/t/031_recovery_conflict.pl
index 875afb8e3ce..b5a5e634f3a 100644
--- a/src/test/recovery/t/031_recovery_conflict.pl
+++ b/src/test/recovery/t/031_recovery_conflict.pl
@@ -68,14 +68,7 @@ $node_primary->wait_for_catchup($node_standby, 'replay', $primary_lsn);
 
 
 # a longrunning psql that we can use to trigger conflicts
-my $psql_timeout = IPC::Run::timer($PostgreSQL::Test::Utils::timeout_default);
-my %psql_standby = ('stdin' => '', 'stdout' => '');
-$psql_standby{run} =
-  $node_standby->background_psql($test_db, \$psql_standby{stdin},
-	\$psql_standby{stdout},
-	$psql_timeout);
-$psql_standby{stdout} = '';
-
+my $psql_standby = $node_standby->background_psql($test_db);
 my $expected_conflicts = 0;
 
 
@@ -104,15 +97,14 @@ my $cursor1 = "test_recovery_conflict_cursor";
 
 # DECLARE and use a cursor on standby, causing buffer with the only block of
 # the relation to be pinned on the standby
-$psql_standby{stdin} .= qq[
-        BEGIN;
-        DECLARE $cursor1 CURSOR FOR SELECT b FROM $table1;
-        FETCH FORWARD FROM $cursor1;
-        ];
+my $res = $psql_standby->query_safe(qq[
+    BEGIN;
+    DECLARE $cursor1 CURSOR FOR SELECT b FROM $table1;
+    FETCH FORWARD FROM $cursor1;
+]);
 # FETCH FORWARD should have returned a 0 since all values of b in the table
 # are 0
-ok(pump_until_standby(qr/^0$/m),
-	"$sect: cursor with conflicting pin established");
+like($res, qr/^0$/m, "$sect: cursor with conflicting pin established");
 
 # to check the log starting now for recovery conflict messages
 my $log_location = -s $node_standby->logfile;
@@ -128,7 +120,7 @@ $primary_lsn = $node_primary->lsn('flush');
 $node_primary->wait_for_catchup($node_standby, 'replay', $primary_lsn);
 
 check_conflict_log("User was holding shared buffer pin for too long");
-reconnect_and_clear();
+$psql_standby->reconnect_and_clear();
 check_conflict_stat("bufferpin");
 
 
@@ -142,15 +134,12 @@ $primary_lsn = $node_primary->lsn('flush');
 $node_primary->wait_for_catchup($node_standby, 'replay', $primary_lsn);
 
 # DECLARE and FETCH from cursor on the standby
-$psql_standby{stdin} .= qq[
+$res = $psql_standby->query_safe(qq[
         BEGIN;
         DECLARE $cursor1 CURSOR FOR SELECT b FROM $table1;
         FETCH FORWARD FROM $cursor1;
-        ];
-ok( pump_until(
-		$psql_standby{run},     $psql_timeout,
-		\$psql_standby{stdout}, qr/^0$/m,),
-	"$sect: cursor with conflicting snapshot established");
+        ]);
+like($res, qr/^0$/m, "$sect: cursor with conflicting snapshot established");
 
 # Do some HOT updates
 $node_primary->safe_psql($test_db,
@@ -165,7 +154,7 @@ $node_primary->wait_for_catchup($node_standby, 'replay', $primary_lsn);
 
 check_conflict_log(
 	"User query might have needed to see row versions that must be removed");
-reconnect_and_clear();
+$psql_standby->reconnect_and_clear();
 check_conflict_stat("snapshot");
 
 
@@ -174,12 +163,12 @@ $sect = "lock conflict";
 $expected_conflicts++;
 
 # acquire lock to conflict with
-$psql_standby{stdin} .= qq[
+$res = $psql_standby->query_safe(qq[
         BEGIN;
         LOCK TABLE $table1 IN ACCESS SHARE MODE;
         SELECT 1;
-        ];
-ok(pump_until_standby(qr/^1$/m), "$sect: conflicting lock acquired");
+        ]);
+like($res, qr/^1$/m, "$sect: conflicting lock acquired");
 
 # DROP TABLE containing block which standby has in a pinned buffer
 $node_primary->safe_psql($test_db, qq[DROP TABLE $table1;]);
@@ -188,7 +177,7 @@ $primary_lsn = $node_primary->lsn('flush');
 $node_primary->wait_for_catchup($node_standby, 'replay', $primary_lsn);
 
 check_conflict_log("User was holding a relation lock for too long");
-reconnect_and_clear();
+$psql_standby->reconnect_and_clear();
 check_conflict_stat("lock");
 
 
@@ -199,14 +188,14 @@ $expected_conflicts++;
 # DECLARE a cursor for a query which, with sufficiently low work_mem, will
 # spill tuples into temp files in the temporary tablespace created during
 # setup.
-$psql_standby{stdin} .= qq[
+$res = $psql_standby->query_safe(qq[
         BEGIN;
         SET work_mem = '64kB';
         DECLARE $cursor1 CURSOR FOR
           SELECT count(*) FROM generate_series(1,6000);
         FETCH FORWARD FROM $cursor1;
-        ];
-ok(pump_until_standby(qr/^6000$/m),
+        ]);
+like($res, qr/^6000$/m,
 	"$sect: cursor with conflicting temp file established");
 
 # Drop the tablespace currently containing spill files for the query on the
@@ -218,7 +207,7 @@ $node_primary->wait_for_catchup($node_standby, 'replay', $primary_lsn);
 
 check_conflict_log(
 	"User was or might have been using tablespace that must be dropped");
-reconnect_and_clear();
+$psql_standby->reconnect_and_clear();
 check_conflict_stat("tablespace");
 
 
@@ -234,7 +223,7 @@ $node_standby->adjust_conf(
 	'max_standby_streaming_delay',
 	"${PostgreSQL::Test::Utils::timeout_default}s");
 $node_standby->restart();
-reconnect_and_clear();
+$psql_standby->reconnect_and_clear();
 
 # Generate a few dead rows, to later be cleaned up by vacuum. Then acquire a
 # lock on another relation in a prepared xact, so it's held continuously by
@@ -258,19 +247,15 @@ SELECT txid_current();
 $primary_lsn = $node_primary->lsn('flush');
 $node_primary->wait_for_catchup($node_standby, 'replay', $primary_lsn);
 
-$psql_standby{stdin} .= qq[
+$res = $psql_standby->query_until(qr/^1$/m, qq[
     BEGIN;
     -- hold pin
     DECLARE $cursor1 CURSOR FOR SELECT a FROM $table1;
     FETCH FORWARD FROM $cursor1;
     -- wait for lock held by prepared transaction
 	SELECT * FROM $table2;
-    ];
-ok( pump_until(
-		$psql_standby{run},     $psql_timeout,
-		\$psql_standby{stdout}, qr/^1$/m,),
-	"$sect: cursor holding conflicting pin, also waiting for lock, established"
-);
+    ]);
+ok( 1, "$sect: cursor holding conflicting pin, also waiting for lock, established");
 
 # just to make sure we're waiting for lock already
 ok( $node_standby->poll_query_until(
@@ -286,7 +271,7 @@ $primary_lsn = $node_primary->lsn('flush');
 $node_primary->wait_for_catchup($node_standby, 'replay', $primary_lsn);
 
 check_conflict_log("User transaction caused buffer deadlock with recovery.");
-reconnect_and_clear();
+$psql_standby->reconnect_and_clear();
 check_conflict_stat("deadlock");
 
 # clean up for next tests
@@ -294,7 +279,7 @@ $node_primary->safe_psql($test_db, qq[ROLLBACK PREPARED 'lock';]);
 $node_standby->adjust_conf('postgresql.conf', 'max_standby_streaming_delay',
 	'50ms');
 $node_standby->restart();
-reconnect_and_clear();
+$psql_standby->reconnect_and_clear();
 
 
 # Check that expected number of conflicts show in pg_stat_database. Needs to
@@ -319,8 +304,7 @@ check_conflict_log("User was connected to a database that must be dropped");
 
 # explicitly shut down psql instances gracefully - to avoid hangs or worse on
 # windows
-$psql_standby{stdin} .= "\\q\n";
-$psql_standby{run}->finish;
+$psql_standby->quit;
 
 $node_standby->stop();
 $node_primary->stop();
@@ -328,37 +312,6 @@ $node_primary->stop();
 
 done_testing();
 
-
-sub pump_until_standby
-{
-	my $match = shift;
-
-	return pump_until($psql_standby{run}, $psql_timeout,
-		\$psql_standby{stdout}, $match);
-}
-
-sub reconnect_and_clear
-{
-	# 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:".
-	$psql_standby{run}->pump_nb();
-	if ($psql_standby{run}->pumpable())
-	{
-		$psql_standby{stdin} .= "\\q\n";
-	}
-	$psql_standby{run}->finish;
-
-	# restart
-	$psql_standby{run}->run();
-	$psql_standby{stdin}  = '';
-	$psql_standby{stdout} = '';
-
-	# Run query to ensure connection has finished re-establishing
-	$psql_standby{stdin} .= qq[SELECT 1;\n];
-	die unless pump_until_standby(qr/^1$/m);
-	$psql_standby{stdout} = '';
-}
-
 sub check_conflict_log
 {
 	my $message          = shift;
diff --git a/src/test/subscription/t/015_stream.pl b/src/test/subscription/t/015_stream.pl
index 91e8aa8c0a5..7d3c893df23 100644
--- a/src/test/subscription/t/015_stream.pl
+++ b/src/test/subscription/t/015_stream.pl
@@ -28,26 +28,20 @@ sub test_streaming
 	my ($node_publisher, $node_subscriber, $appname, $is_parallel) = @_;
 
 	# Interleave a pair of transactions, each exceeding the 64kB limit.
-	my $in  = '';
-	my $out = '';
-
 	my $offset = 0;
 
-	my $timer = IPC::Run::timeout($PostgreSQL::Test::Utils::timeout_default);
-
-	my $h = $node_publisher->background_psql('postgres', \$in, \$out, $timer,
+	my $h = $node_publisher->background_psql('postgres',
 		on_error_stop => 0);
 
 	# Check the subscriber log from now on.
 	$offset = -s $node_subscriber->logfile;
 
-	$in .= q{
+	$h->query_safe(q{
 	BEGIN;
 	INSERT INTO test_tab SELECT i, md5(i::text) FROM generate_series(3, 5000) s(i);
 	UPDATE test_tab SET b = md5(b) WHERE mod(a,2) = 0;
 	DELETE FROM test_tab WHERE mod(a,3) = 0;
-	};
-	$h->pump_nb;
+	});
 
 	$node_publisher->safe_psql(
 		'postgres', q{
@@ -57,11 +51,10 @@ sub test_streaming
 	COMMIT;
 	});
 
-	$in .= q{
-	COMMIT;
-	\q
-	};
-	$h->finish;    # errors make the next test fail, so ignore them here
+	$h->query_safe('COMMIT');
+	$h->quit;
+	# XXX: Not sure what this means
+    # errors make the next test fail, so ignore them here
 
 	$node_publisher->wait_for_catchup($appname);
 
@@ -219,12 +212,7 @@ $node_subscriber->reload;
 $node_subscriber->safe_psql('postgres', q{SELECT 1});
 
 # Interleave a pair of transactions, each exceeding the 64kB limit.
-my $in  = '';
-my $out = '';
-
-my $timer = IPC::Run::timeout($PostgreSQL::Test::Utils::timeout_default);
-
-my $h = $node_publisher->background_psql('postgres', \$in, \$out, $timer,
+my $h = $node_publisher->background_psql('postgres',
 	on_error_stop => 0);
 
 # Confirm if a deadlock between the leader apply worker and the parallel apply
@@ -232,11 +220,10 @@ my $h = $node_publisher->background_psql('postgres', \$in, \$out, $timer,
 
 my $offset = -s $node_subscriber->logfile;
 
-$in .= q{
+$h->query_safe(q{
 BEGIN;
 INSERT INTO test_tab_2 SELECT i FROM generate_series(1, 5000) s(i);
-};
-$h->pump_nb;
+});
 
 # Ensure that the parallel apply worker executes the insert command before the
 # leader worker.
@@ -246,11 +233,8 @@ $node_subscriber->wait_for_log(
 
 $node_publisher->safe_psql('postgres', "INSERT INTO test_tab_2 values(1)");
 
-$in .= q{
-COMMIT;
-\q
-};
-$h->finish;
+$h->query_safe('COMMIT');
+$h->quit;
 
 $node_subscriber->wait_for_log(qr/ERROR: ( [A-Z0-9]+:)? deadlock detected/,
 	$offset);
@@ -277,11 +261,10 @@ $node_subscriber->safe_psql('postgres',
 # Check the subscriber log from now on.
 $offset = -s $node_subscriber->logfile;
 
-$in .= q{
+$h->query_safe(q{
 BEGIN;
 INSERT INTO test_tab_2 SELECT i FROM generate_series(1, 5000) s(i);
-};
-$h->pump_nb;
+});
 
 # Ensure that the first parallel apply worker executes the insert command
 # before the second one.
@@ -292,11 +275,8 @@ $node_subscriber->wait_for_log(
 $node_publisher->safe_psql('postgres',
 	"INSERT INTO test_tab_2 SELECT i FROM generate_series(1, 5000) s(i)");
 
-$in .= q{
-COMMIT;
-\q
-};
-$h->finish;
+$h->query_safe('COMMIT');
+$h->quit;
 
 $node_subscriber->wait_for_log(qr/ERROR: ( [A-Z0-9]+:)? deadlock detected/,
 	$offset);
-- 
2.38.0

