1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
|
# 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::BackgroundPsql->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 doesn'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} = 1;
return $self->{query_timer_restart};
}
1;
|