File Coverage

blib/lib/Test/PostgreSQL.pm
Criterion Covered Total %
statement 85 321 26.4
branch 17 208 8.1
condition 1 44 2.2
subroutine 20 37 54.0
pod 7 10 70.0
total 130 620 20.9


line stmt bran cond sub pod time code
1             package Test::PostgreSQL;
2 11     11   651346 use 5.14.0;
  11         109  
3 11     11   47 use strict;
  11         17  
  11         202  
4 11     11   53 use warnings;
  11         18  
  11         280  
5 11     11   4860 use Moo;
  11         102775  
  11         51  
6 11     11   18610 use Types::Standard -all;
  11         668156  
  11         105  
7 11     11   437548 use Function::Parameters qw(:strict);
  11         33747  
  11         54  
8 11     11   8979 use Try::Tiny;
  11         12185  
  11         538  
9 11     11   1344 use DBI;
  11         14973  
  11         342  
10 11     11   60 use File::Spec;
  11         19  
  11         199  
11 11     11   6895 use File::Temp;
  11         176648  
  11         752  
12 11     11   4135 use File::Which;
  11         9080  
  11         595  
13 11     11   4707 use POSIX qw(SIGQUIT SIGKILL WNOHANG getuid setuid);
  11         58167  
  11         70  
14 11     11   18395 use User::pwent;
  11         55684  
  11         47  
15              
16             our $VERSION = '1.26';
17             our $errstr;
18              
19             # Deprecate use of %Defaults as we want to remove this package global
20 11     11   5521 use Tie::Hash::Method;
  11         79434  
  11         4599  
21             tie our %Defaults, 'Tie::Hash::Method', FETCH => sub {
22             my $msg = "\nWARNING: using \$Test::PostgreSQL::Defaults is DEPRECATED.";
23             if ( $_[1] =~ /^(initdb|postmaster)_args$/ ) {
24             $msg .= " Use Test::PostgreSQL->new( extra_$_[1] => ... ) instead.";
25             }
26             warn $msg;
27             return $_[0]->base_hash->{ $_[1] };
28             };
29              
30             %Defaults = (
31             auto_start => 2,
32             initdb_args => '-U postgres -A trust',
33             postmaster_args => '-h 127.0.0.1 -F',
34             );
35              
36             has dbname => (
37             is => 'ro',
38             isa => Str,
39             default => 'test',
40             );
41              
42             has dbowner => (
43             is => 'ro',
44             isa => Str,
45             default => 'postgres',
46             );
47              
48             has host => (
49             is => 'ro',
50             isa => Str,
51             default => '127.0.0.1',
52             );
53              
54             # Various paths that Postgres gets installed under, sometimes with a version on the end,
55             # in which case take the highest version. We append /bin/ and so forth to the path later.
56             # *Note that these are used only if the program isn't already in the path!*
57             has search_paths => (
58             is => "ro",
59             isa => ArrayRef,
60             builder => "_search_paths",
61             );
62              
63 10 50   10   2157 method _search_paths() {
  10 50       40  
  10         21  
  10         14  
64             my @base_paths = (
65             # popular installation dir?
66             qw(/usr/local/pgsql),
67             # ubuntu (maybe debian as well, find the newest version)
68 0         0 (sort { $b cmp $a } grep { -d $_ } glob "/usr/lib/postgresql/*"),
  0         0  
69             # macport
70 0         0 (sort { $b cmp $a } grep { -d $_ } glob "/opt/local/lib/postgresql*"),
  0         0  
71             # Postgresapp.com
72 10         576 (sort { $b cmp $a } grep { -d $_ } glob "/Applications/Postgres.app/Contents/Versions/*"),
  0         0  
  0         0  
73             # BSDs end up with it in /usr/local/bin which doesn't appear to be in the path sometimes:
74             "/usr/local",
75             );
76              
77             # This environment variable is used to override the default, so it gets
78             # prefixed to the start of the search paths.
79 10 50       57 if (defined $ENV{POSTGRES_HOME}) {
80 0         0 return [$ENV{POSTGRES_HOME}, @base_paths];
81             }
82 10         201 return \@base_paths;
83             }
84              
85             # We attempt to use this port first, and will increment from there.
86             # The final port ends up in the ->port attribute.
87             has base_port => (
88             is => "ro",
89             isa => Int,
90             default => 15432,
91             );
92              
93             has auto_start => (
94             is => "ro",
95             default => 2,
96             );
97              
98             has base_dir => (
99             is => "rw",
100             default => sub {
101             File::Temp->newdir(
102             'pgtest.XXXXX',
103             CLEANUP => $ENV{TEST_POSTGRESQL_PRESERVE} ? undef : 1,
104             EXLOCK => 0,
105             TMPDIR => 1
106             );
107             },
108             coerce => fun ($newval) {
109             # Ensure base_dir is absolute; usually only the case if the user set it.
110             # Avoid munging objects such as File::Temp
111             ref $newval ? $newval : File::Spec->rel2abs($newval);
112             },
113             );
114              
115             has socket_dir => (
116             is => "ro",
117             isa => Str,
118             lazy => 1,
119             default => method () { File::Spec->catdir( $self->base_dir, 'tmp' ) },
120             );
121              
122             has initdb => (
123             is => "ro",
124             isa => Str,
125             lazy => 1,
126             default => method () { $self->_find_program('initdb') || die $errstr },
127             );
128              
129             has initdb_args => (
130             is => "lazy",
131             isa => Str,
132             );
133              
134 0 0   0   0 method _build_initdb_args() {
  0 0       0  
  0         0  
  0         0  
135 0         0 return '-U '. $self->dbowner . ' -A trust ' . $self->extra_initdb_args;
136             }
137              
138             has extra_initdb_args => (
139             is => "ro",
140             isa => Str,
141             default => "",
142             );
143              
144             has unix_socket => (
145             is => "ro",
146             isa => Bool,
147             default => 0,
148             );
149              
150             has pg_ctl => (
151             is => "ro",
152             isa => Maybe[Str],
153             lazy => 1,
154             builder => "_pg_ctl_builder",
155             );
156              
157 10 50   10   127 method _pg_ctl_builder() {
  10 50       43  
  10         24  
  10         14  
158 10         43 my $prog = $self->_find_program('pg_ctl');
159 10 50       29 if ( $prog ) {
160             # we only use pg_ctl if Pg version is >= 9
161 0         0 my $ret = qx/"$prog" --version/;
162 0 0 0     0 if ( $ret =~ /(\d+)(?:\.|devel)/ && $1 >= 9 ) {
163 0         0 return $prog;
164             }
165 0         0 warn "pg_ctl version earlier than 9";
166 0         0 return;
167             }
168 10         197 return;
169             }
170              
171             has pg_config => (
172             is => 'ro',
173             isa => Str,
174             );
175              
176             has psql => (
177             is => 'ro',
178             isa => Str,
179             lazy => 1,
180             default => method () { $self->_find_program('psql') || die $errstr },
181             );
182              
183             has psql_args => (
184             is => 'lazy',
185             isa => Str,
186             );
187              
188 0 0   0   0 method _build_psql_args() {
  0 0       0  
  0         0  
  0         0  
189 0 0       0 return '-U ' . $self->dbowner . ' -d ' . $self->dbname . ' -h '.
190             ($self->unix_socket ? $self->socket_dir : '127.0.0.1') .
191             ' -p ' . $self->port
192             . $self->extra_psql_args;
193             }
194              
195             has extra_psql_args => (
196             is => 'ro',
197             isa => Str,
198             default => '',
199             );
200              
201             has run_psql_args => (
202             is => 'ro',
203             isa => Str,
204             # Single transaction, skip .psqlrc, be quiet, echo errors, stop on first error
205             default => '-1Xqb -v ON_ERROR_STOP=1',
206             );
207              
208             has seed_scripts => (
209             is => 'ro',
210             isa => ArrayRef[Str],
211             default => sub { [] },
212             );
213              
214             has pid => (
215             is => "rw",
216             isa => Maybe[Int],
217             );
218              
219             has port => (
220             is => "rw",
221             isa => Maybe[Int],
222             );
223              
224             has uid => (
225             is => "rw",
226             isa => Maybe[Int],
227             );
228              
229             # Are we running as root? (Typical when run inside Docker containers)
230             has is_root => (
231             is => "ro",
232             isa => Bool,
233             default => sub { getuid == 0 }
234             );
235              
236             has postmaster => (
237             is => "rw",
238             isa => Str,
239             lazy => 1,
240             default => method () {
241             $self->_find_program("postgres")
242             || $self->_find_program("postmaster")
243             || die $errstr
244             },
245             );
246              
247             has postmaster_args => (
248             is => "lazy",
249             isa => Str,
250             );
251              
252 0 0   0   0 method _build_postmaster_args() {
  0 0       0  
  0         0  
  0         0  
253 0 0       0 return "-h ".
254             ($self->unix_socket ? "''" : "127.0.0.1") .
255             " -F " . $self->extra_postmaster_args;
256             }
257              
258             has extra_postmaster_args => (
259             is => "ro",
260             isa => Str,
261             default => "",
262             );
263              
264             has _owner_pid => (
265             is => "ro",
266             isa => Int,
267             default => sub { $$ },
268             );
269              
270 10 50   10 0 464 method BUILD($) {
  10 50       52  
  10         22  
  10         25  
  10         18  
271             # Ensure we have one or the other ways of starting Postgres:
272 10 50   10   91 try { $self->pg_ctl or $self->postmaster } catch { die $_ };
  10         662  
  10         205  
273              
274 0 0 0     0 if (defined $self->uid and $self->uid == 0) {
275 0         0 die "uid() must be set to a non-root user id.";
276             }
277              
278 0 0 0     0 if (not defined($self->uid) and $self->is_root) {
279 0         0 my $ent = getpwnam("nobody");
280 0 0       0 unless (defined $ent) {
281 0         0 die "user nobody does not exist, use uid() to specify a non-root user.";
282             }
283 0 0       0 unless ($ent->uid > 0) {
284 0         0 die "user nobody has uid 0; confused and exiting. use uid() to specify a non-root user.";
285             }
286 0         0 $self->uid($ent->uid);
287             }
288              
289             # Ensure base dir is writable by our target uid, if we were running as root
290 0 0       0 chown $self->uid, -1, $self->base_dir
291             if defined $self->uid;
292              
293 0 0       0 if ($self->auto_start) {
294 0 0       0 $self->setup
295             if $self->auto_start >= 2;
296 0         0 $self->start;
297             }
298             }
299              
300 10 50   10 0 9919 method DEMOLISH($in_global_destruction) {
  10 50       33  
  10         20  
  10         37  
  10         16  
301 10         33 local $?;
302 10 50 33     161 if (defined $self->pid && $self->_owner_pid == $$) {
303 0         0 $self->stop
304             }
305 10         326 return;
306             }
307              
308             sub dsn {
309 0     0 1 0 my %args = shift->_default_args(@_);
310              
311 0         0 return 'DBI:Pg:' . join(';', map { "$_=$args{$_}" } sort keys %args);
  0         0  
312             }
313              
314             sub _default_args {
315 0     0   0 my ($self, %args) = @_;
316             # If we're doing socket-only (i.e., not listening on localhost),
317             # then provide the path to the socket
318 0 0       0 if ($self->{unix_socket}) {
319 0   0     0 $args{host} //= $self->socket_dir;
320             } else {
321 0   0     0 $args{host} ||= $self->host;
322             }
323              
324 0   0     0 $args{port} ||= $self->port;
325 0   0     0 $args{user} ||= $self->dbowner;
326 0   0     0 $args{dbname} ||= $self->dbname;
327 0         0 return %args;
328             }
329              
330             sub uri {
331 0     0 1 0 my $self = shift;
332 0         0 my %args = $self->_default_args(@_);
333              
334 0         0 return sprintf('postgresql://%s@%s:%d/%s', @args{qw/user host port dbname/});
335             }
336              
337 0 0   0 1 0 method start() {
  0 0       0  
  0         0  
  0         0  
338 0 0       0 if (defined $self->pid) {
339 0         0 warn "Apparently already started on " . $self->pid . "; not restarting.";
340 0         0 return;
341             }
342              
343             # If the user specified a port, try only that port:
344 0 0       0 if ($self->port) {
345 0         0 $self->_try_start($self->port);
346             }
347             else {
348 0         0 $self->_find_port_and_launch;
349             }
350              
351             # create "test" database
352 0         0 $self->_create_test_database($self->dbname);
353             }
354              
355             # This whole method was mostly cargo-culted from the earlier test-postgresql;
356             # It could probably be made more sane.
357 0 0   0   0 method _find_port_and_launch() {
  0 0       0  
  0         0  
  0         0  
358 0         0 my $tries = 10;
359 0         0 my $port = $self->base_port;
360             # try by incrementing port number
361 0         0 while (1) {
362             my $good = try {
363 0     0   0 $self->_try_start($port);
364 0         0 1;
365             }
366             catch {
367             # warn "Postgres failed to start on port $port\n";
368 0 0   0   0 unless ($tries--) {
369 0         0 die "Failed to start postgres on port $port: $_";
370             }
371 0         0 undef;
372 0         0 };
373 0 0       0 return if $good;
374 0         0 $port++;
375             }
376             }
377              
378 0 0   0   0 method _try_start($port) {
  0 0       0  
  0         0  
  0         0  
  0         0  
379 0         0 my $logfile = File::Spec->catfile($self->base_dir, 'postgres.log');
380              
381 0 0       0 if ( $self->pg_ctl ) {
382 0         0 my @cmd = (
383             $self->pg_ctl,
384             'start', '-w', '-s', '-D',
385             File::Spec->catdir( $self->base_dir, 'data' ),
386             '-l', $logfile, '-o',
387             join( ' ',
388             $self->postmaster_args, '-p',
389             $port, '-k',
390             $self->socket_dir)
391             );
392 0         0 $self->setuid_cmd(\@cmd, 1);
393              
394 0         0 my $pid_path = File::Spec->catfile( $self->base_dir, 'data', 'postmaster.pid' );
395              
396 0 0       0 open( my $pidfh, '<', $pid_path )
397             or die "Failed to open $pid_path: $!";
398              
399             # Note that the file contains several lines; we only want the PID from the first.
400 0         0 my $pid = <$pidfh>;
401 0         0 chomp $pid;
402 0         0 $self->pid($pid);
403 0         0 close $pidfh;
404              
405 0         0 $self->port($port);
406             }
407             else {
408             # old style - open log and fork
409 0 0       0 open my $logfh, '>>', $logfile
410             or die "failed to create log file: $logfile: $!";
411 0         0 my $pid = fork;
412 0 0       0 die "fork(2) failed:$!"
413             unless defined $pid;
414 0 0       0 if ($pid == 0) {
415 0 0       0 open STDOUT, '>>&', $logfh
416             or die "dup(2) failed:$!";
417 0 0       0 open STDERR, '>>&', $logfh
418             or die "dup(2) failed:$!";
419 0 0       0 chdir $self->base_dir
420             or die "failed to chdir to:" . $self->base_dir . ":$!";
421 0 0       0 if (defined $self->uid) {
422 0 0       0 setuid($self->uid) or die "setuid failed: $!";
423             }
424 0         0 my $cmd = join(
425             ' ',
426             $self->postmaster,
427             $self->postmaster_args,
428             '-p', $port,
429             '-D', File::Spec->catdir($self->base_dir, 'data'),
430             '-k', $self->socket_dir,
431             );
432 0         0 exec($cmd);
433 0         0 die "failed to launch postmaster:$?";
434             }
435 0         0 close $logfh;
436             # wait until server becomes ready (or dies)
437 0         0 for (my $i = 0; $i < 100; $i++) {
438 0 0       0 open $logfh, '<', $logfile
439             or die "failed to create log file: $logfile: $!";
440 0         0 my $lines = do { join '', <$logfh> };
  0         0  
441 0         0 close $logfh;
442             last
443 0 0       0 if $lines =~ /is ready to accept connections/;
444 0 0       0 if (waitpid($pid, WNOHANG) > 0) {
445             # failed
446 0         0 die "Failed to start Postgres: $lines\n";
447             }
448 0         0 sleep 1;
449             }
450             # PostgreSQL is ready
451 0         0 $self->pid($pid);
452 0         0 $self->port($port);
453             }
454 0         0 return;
455             }
456              
457 0 0   0 1 0 method stop($sig = SIGQUIT) {
  0 0       0  
  0 0       0  
  0         0  
  0         0  
458 0 0 0     0 if ( $self->pg_ctl && defined $self->base_dir ) {
459 0         0 my @cmd = (
460             $self->pg_ctl, 'stop', '-s', '-D',
461             File::Spec->catdir( $self->base_dir, 'data' ),
462             '-m', 'fast'
463             );
464 0         0 $self->setuid_cmd(\@cmd);
465             }
466             else {
467             # old style or $self->base_dir File::Temp obj already DESTROYed
468 0 0       0 return unless defined $self->pid;
469              
470 0         0 kill $sig, $self->pid;
471 0         0 my $timeout = 10;
472 0   0     0 while ($timeout > 0 and waitpid($self->pid, WNOHANG) == 0) {
473 0         0 $timeout -= sleep(1);
474             }
475              
476 0 0       0 if ($timeout <= 0) {
477 0         0 warn "Pg refused to die gracefully; killing it violently.\n";
478 0         0 kill SIGKILL, $self->pid;
479 0         0 $timeout = 5;
480 0   0     0 while ($timeout > 0 and waitpid($self->pid, WNOHANG) == 0) {
481 0         0 $timeout -= sleep(1);
482             }
483 0 0       0 if ($timeout <= 0) {
484 0         0 warn "Pg really didn't die.. WTF?\n";
485             }
486             }
487             }
488 0         0 $self->pid(undef);
489 0         0 return;
490             }
491              
492 0 0   0   0 method _create_test_database($dbname) {
  0 0       0  
  0         0  
  0         0  
  0         0  
493 0         0 my $tries = 5;
494 0         0 my $dbh;
495 0         0 while ($tries) {
496 0         0 $tries -= 1;
497 0         0 $dbh = DBI->connect($self->dsn(dbname => 'template1'), '', '', {
498             PrintError => 0,
499             RaiseError => 0
500             });
501 0 0       0 last if $dbh;
502              
503             # waiting for database to start up
504 0 0 0     0 if ($DBI::errstr =~ /the database system is starting up/
505             || $DBI::errstr =~ /Connection refused/) {
506 0         0 sleep(1);
507 0         0 next;
508             }
509 0         0 die $DBI::errstr;
510             }
511              
512 0 0       0 die "Connection to the database failed even after 5 tries"
513             unless ($dbh);
514              
515 0 0       0 if ($dbh->selectrow_arrayref(qq{SELECT COUNT(*) FROM pg_database WHERE datname='$dbname'})->[0] == 0) {
516 0 0       0 $dbh->do("CREATE DATABASE $dbname")
517             or die $dbh->errstr;
518             }
519              
520 0   0     0 my $seed_scripts = $self->seed_scripts || [];
521            
522 0 0       0 $self->run_psql_scripts(@$seed_scripts)
523             if @$seed_scripts;
524            
525 0         0 return;
526             }
527              
528 0 0   0 1 0 method setup() {
  0 0       0  
  0         0  
  0         0  
529             # (re)create directory structure
530 0         0 mkdir $self->base_dir;
531 0 0       0 chmod 0755, $self->base_dir
532             or die "failed to chmod 0755 dir:" . $self->base_dir . ":$!";
533 0 0 0     0 if ($ENV{USER} && $ENV{USER} eq 'root') {
534 0 0       0 chown $self->uid, -1, $self->base_dir
535             or die "failed to chown dir:" . $self->base_dir . ":$!";
536             }
537 0         0 my $tmpdir = $self->socket_dir;
538 0 0       0 if (mkdir $tmpdir) {
539 0 0       0 if ($self->uid) {
540 0 0       0 chown $self->uid, -1, $tmpdir
541             or die "failed to chown dir:$tmpdir:$!";
542             }
543             }
544             # initdb
545 0 0       0 if (! -d File::Spec->catdir($self->base_dir, 'data')) {
546 0 0       0 if ( $self->pg_ctl ) {
547 0         0 my @cmd = (
548             $self->pg_ctl,
549             'init',
550             '-s',
551             '-D', File::Spec->catdir($self->base_dir, 'data'),
552             '-o',
553             $self->initdb_args,
554             );
555 0         0 $self->setuid_cmd(\@cmd);
556             }
557             else {
558             # old style
559 0 0       0 pipe my $rfh, my $wfh
560             or die "failed to create pipe:$!";
561 0         0 my $pid = fork;
562 0 0       0 die "fork failed:$!"
563             unless defined $pid;
564 0 0       0 if ($pid == 0) {
565 0         0 close $rfh;
566 0 0       0 open STDOUT, '>&', $wfh
567             or die "dup(2) failed:$!";
568 0 0       0 open STDERR, '>&', $wfh
569             or die "dup(2) failed:$!";
570 0 0       0 chdir $self->base_dir
571             or die "failed to chdir to:" . $self->base_dir . ":$!";
572 0 0       0 if (defined $self->uid) {
573 0 0       0 setuid($self->uid)
574             or die "setuid failed:$!";
575             }
576 0         0 my $cmd = join(
577             ' ',
578             $self->initdb,
579             $self->initdb_args,
580             '-D', File::Spec->catdir($self->base_dir, 'data'),
581             );
582 0         0 exec($cmd);
583 0         0 die "failed to exec:$cmd:$!";
584             }
585 0         0 close $wfh;
586 0         0 my $output = '';
587 0         0 while (my $l = <$rfh>) {
588 0         0 $output .= $l;
589             }
590 0         0 close $rfh;
591 0         0 while (waitpid($pid, 0) <= 0) {
592             }
593 0 0       0 die "*** initdb failed ***\n$output\n"
594             if $? != 0;
595              
596             }
597            
598 0         0 my $conf_file
599             = File::Spec->catfile($self->base_dir, 'data', 'postgresql.conf');
600            
601 0 0       0 if (my $pg_config = $self->pg_config) {
602 0 0       0 open my $fh, '>', $conf_file or die "Can't open $conf_file: $!";
603 0         0 print $fh $pg_config;
604 0         0 close $fh;
605             }
606             else {
607             # use postgres hard-coded configuration as some packagers mess
608             # around with postgresql.conf.sample too much:
609 0         0 truncate $conf_file, 0;
610             }
611             }
612             }
613              
614 30 50   30   64 method _find_program($prog) {
  30 50       64  
  30         40  
  30         57  
  30         38  
615 30         45 undef $errstr;
616 30         91 my $path = which $prog;
617 30 50       4887 return $path if $path;
618 30         45 for my $sp (@{$self->search_paths}) {
  30         114  
619 60 50       427 return "$sp/bin/$prog" if -x "$sp/bin/$prog";
620 60 50       508 return "$sp/$prog" if -x "$sp/$prog";
621             }
622 30         107 $errstr = "could not find $prog, please set appropriate PATH or POSTGRES_HOME";
623 30         236 return;
624             }
625              
626 0 0   0 0   method setuid_cmd($cmd, $suppress_errors = !1) {
  0 0          
  0 0          
  0            
  0            
627 0           my $pid = fork;
628 0 0         if ($pid == 0) {
629 0           chdir $self->base_dir;
630 0 0         if (defined $self->uid) {
631 0 0         setuid($self->uid) or die "setuid failed: $!";
632             }
633 0 0         close STDERR if $suppress_errors;
634 0 0         exec(@$cmd) or die "Failed to exec pg_ctl: $!";
635             }
636             else {
637 0           waitpid($pid, 0);
638             }
639             }
640              
641 0 0   0 1   method run_psql(@psql_args) {
  0            
  0            
  0            
642 0           my $cmd = join ' ', (
643             $self->psql,
644            
645             # Default connection settings
646             $self->psql_args,
647            
648             # Extra connection settings or something else
649             $self->extra_psql_args,
650            
651             # run_psql specific arguments
652             $self->run_psql_args,
653            
654             @psql_args,
655             );
656            
657             # Usually anything less than WARNING is not really helpful
658             # in batch mode. Does it make sense to make this configurable?
659 0           local $ENV{PGOPTIONS} = '--client-min-messages=warning';
660            
661 0           my $psql_out = qx{$cmd 2>&1};
662            
663 0 0         die "Error executing psql: $psql_out" unless $? == 0;
664             }
665              
666 0 0   0 1   method run_psql_scripts(@script_paths) {
  0            
  0            
  0            
667 0           my $psql_args = join ' ', map {; "-f $_" } @script_paths;
  0            
668            
669 0           $self->run_psql($psql_args);
670             }
671              
672             1;
673             __END__