File Coverage

blib/lib/Test/Postgresql58.pm
Criterion Covered Total %
statement 43 163 26.3
branch 12 110 10.9
condition 2 26 7.6
subroutine 11 19 57.8
pod 6 6 100.0
total 74 324 22.8


line stmt bran cond sub pod time code
1             package Test::Postgresql58;
2              
3 7     7   134660 use strict;
  7         11  
  7         166  
4 7     7   21 use warnings;
  7         8  
  7         152  
5              
6 7     7   121 use 5.008;
  7         15  
7 7     7   2923 use Class::Accessor::Lite;
  7         6134  
  7         35  
8 7     7   264 use Cwd;
  7         8  
  7         376  
9 7     7   1482 use DBI;
  7         12111  
  7         1297  
10 7     7   4499 use File::Temp qw(tempdir);
  7         103143  
  7         405  
11 7     7   2841 use POSIX qw(SIGTERM SIGKILL WNOHANG setuid);
  7         29745  
  7         33  
12              
13             our $VERSION = '2.01';
14              
15             # Various paths that Postgres gets installed under, sometimes with a version on the end,
16             # in which case take the highest version. We append /bin/ and so forth to the path later.
17             # Note that these are used only if the program isn't already in the path.
18             our @SEARCH_PATHS = (
19             split(/:/, $ENV{PATH}),
20             # popular installation dir?
21             qw(/usr/local/pgsql),
22             # ubuntu (maybe debian as well, find the newest version)
23             (sort { $b cmp $a } grep { -d $_ } glob "/usr/lib/postgresql/*"),
24             # macport
25             (sort { $b cmp $a } grep { -d $_ } glob "/opt/local/lib/postgresql*"),
26             # Postgresapp.com
27             (sort { $b cmp $a } grep { -d $_ } glob "/Applications/Postgres.app/Contents/Versions/*"),
28             # BSDs end up with it in /usr/local/bin which doesn't appear to be in the path sometimes:
29             "/usr/local",
30             );
31              
32             # This environment variable is used to override the default, so it gets
33             # prefixed to the start of the search paths.
34             if (defined $ENV{POSTGRES_HOME} and -d $ENV{POSTGRES_HOME}) {
35             unshift @SEARCH_PATHS, $ENV{POSTGRES_HOME};
36             }
37              
38             our $errstr;
39             our $BASE_PORT = 15432;
40              
41             our %Defaults = (
42             auto_start => 2,
43             base_dir => undef,
44             initdb => undef,
45             initdb_args => '-U postgres -A trust',
46             pid => undef,
47             port => undef,
48             postmaster => undef,
49             postmaster_args => '-h 127.0.0.1 -F',
50             uid => undef,
51             _owner_pid => undef,
52             );
53              
54             Class::Accessor::Lite->mk_accessors(keys %Defaults);
55              
56             sub new {
57 6     6 1 1204 my $klass = shift;
58             my $self = bless {
59             %Defaults,
60 6 50       95 @_ == 1 ? %{$_[0]} : @_,
  0         0  
61             _owner_pid => $$,
62             }, $klass;
63 6 50 33     34 if (! defined $self->uid && $ENV{USER} eq 'root') {
64 0 0       0 my @a = getpwnam('nobody')
65             or die "user nobody does not exist, use uid() to specify user:$!";
66 0         0 $self->uid($a[2]);
67             }
68 6 100       603 if (defined $self->base_dir) {
69 1 50       8 $self->base_dir(cwd . '/' . $self->base_dir)
70             if $self->base_dir !~ m|^/|;
71             } else {
72             $self->base_dir(
73             tempdir(
74 5 50       67 CLEANUP => $ENV{TEST_POSTGRESQL_PRESERVE} ? undef : 1,
75             ),
76             );
77 5 50       2586 chown $self->uid, -1, $self->base_dir
78             if defined $self->uid;
79             }
80 6 50       50 if (! defined $self->initdb) {
81 6 50       44 my $prog = _find_program('initdb')
82             or return;
83 0         0 $self->initdb($prog);
84             }
85 0 0       0 if (! defined $self->postmaster) {
86 0 0       0 my $prog = _find_program('postmaster')
87             or return;
88 0         0 $self->postmaster($prog);
89             }
90 0 0       0 if ($self->auto_start) {
91 0 0       0 $self->setup
92             if $self->auto_start >= 2;
93 0         0 $self->start;
94             }
95 0         0 $self;
96             }
97              
98             sub DESTROY {
99 6     6   24 local $?;
100 6         12 my $self = shift;
101 6 50 33     23 $self->stop
102             if defined $self->pid && $$ == $self->_owner_pid;
103 6         63 return;
104             }
105              
106             sub dsn {
107 0     0 1 0 my $self = shift;
108 0         0 my %args = $self->_default_args(@_);
109              
110 0         0 return 'DBI:Pg:' . join(';', map { "$_=$args{$_}" } sort keys %args);
  0         0  
111             }
112              
113             sub _default_args {
114 0     0   0 my ($self, %args) = @_;
115 0   0     0 $args{host} ||= '127.0.0.1';
116 0   0     0 $args{port} ||= $self->port;
117 0   0     0 $args{user} ||= 'postgres';
118 0   0     0 $args{dbname} ||= 'test';
119 0         0 return %args;
120             }
121              
122             sub uri {
123 0     0 1 0 my $self = shift;
124 0         0 my %args = $self->_default_args(@_);
125              
126 0         0 return sprintf('postgresql://%s@%s:%d/%s', @args{qw/user host port dbname/});
127             }
128              
129             sub start {
130 0     0 1 0 my $self = shift;
131             return
132 0 0       0 if defined $self->pid;
133             # start (or die)
134             sub {
135 0     0   0 my $err;
136 0 0       0 if ($self->port) {
137 0 0       0 $err = $self->_try_start($self->port)
138             or return;
139             } else {
140             # try by incrementing port no
141 0         0 for (my $port = $BASE_PORT; $port < $BASE_PORT + 100; $port++) {
142 0 0       0 $err = $self->_try_start($port)
143             or return;
144             }
145             }
146             # failed
147 0         0 die "failed to launch PostgreSQL:$!\n$err";
148 0         0 }->();
149             { # create "test" database
150 0         0 my $tries = 5;
  0         0  
151 0         0 my $dbh;
152 0         0 while ($tries) {
153 0         0 $tries -= 1;
154 0         0 $dbh = DBI->connect($self->dsn(dbname => 'template1'), '', '', {
155             PrintError => 0,
156             RaiseError => 0
157             });
158 0 0       0 last if $dbh;
159              
160             # waiting for database to start up
161 0 0 0     0 if ($DBI::errstr =~ /the database system is starting up/
162             || $DBI::errstr =~ /Connection refused/) {
163 0         0 sleep(1);
164 0         0 next;
165             }
166 0         0 die $DBI::errstr;
167             }
168              
169 0 0       0 die "Connection to the database failed even after 5 tries"
170             unless ($dbh);
171              
172 0 0       0 if ($dbh->selectrow_arrayref(q{SELECT COUNT(*) FROM pg_database WHERE datname='test'})->[0] == 0) {
173 0 0       0 $dbh->do('CREATE DATABASE test')
174             or die $dbh->errstr;
175             }
176             }
177             }
178              
179             sub _try_start {
180 0     0   0 my ($self, $port) = @_;
181             # open log and fork
182 0 0       0 open my $logfh, '>>', $self->base_dir . '/postgres.log'
183             or die 'failed to create log file:' . $self->base_dir
184             . "/postgres.log:$!";
185 0         0 my $pid = fork;
186 0 0       0 die "fork(2) failed:$!"
187             unless defined $pid;
188 0 0       0 if ($pid == 0) {
189 0 0       0 open STDOUT, '>>&', $logfh
190             or die "dup(2) failed:$!";
191 0 0       0 open STDERR, '>>&', $logfh
192             or die "dup(2) failed:$!";
193 0 0       0 chdir $self->base_dir
194             or die "failed to chdir to:" . $self->base_dir . ":$!";
195 0 0       0 if (defined $self->uid) {
196 0 0       0 setuid($self->uid)
197             or die "setuid failed:$!";
198             }
199 0         0 my $cmd = join(
200             ' ',
201             $self->postmaster,
202             $self->postmaster_args,
203             '-p', $port,
204             '-D', $self->base_dir . '/data',
205             '-k', $self->base_dir . '/tmp',
206             );
207 0         0 exec($cmd);
208 0         0 die "failed to launch postmaster:$?";
209             }
210 0         0 close $logfh;
211             # wait until server becomes ready (or dies)
212 0         0 for (my $i = 0; $i < 100; $i++) {
213 0 0       0 open $logfh, '<', $self->base_dir . '/postgres.log'
214             or die 'failed to open log file:' . $self->base_dir
215             . "/postgres.log:$!";
216 0         0 my $lines = do { join '', <$logfh> };
  0         0  
217 0         0 close $logfh;
218             last
219 0 0       0 if $lines =~ /is ready to accept connections/;
220 0 0       0 if (waitpid($pid, WNOHANG) > 0) {
221             # failed
222 0         0 return $lines;
223             }
224 0         0 sleep 1;
225             }
226             # PostgreSQL is ready
227 0         0 $self->pid($pid);
228 0         0 $self->port($port);
229 0         0 return;
230             }
231              
232             sub stop {
233 0     0 1 0 my ($self, $sig) = @_;
234 0 0       0 return unless defined $self->pid;
235              
236 0   0     0 $sig ||= SIGTERM;
237              
238 0         0 kill $sig, $self->pid;
239 0         0 my $timeout = 10;
240 0   0     0 while ($timeout > 0 and waitpid($self->pid, WNOHANG) <= 0) {
241 0         0 $timeout -= sleep(1);
242             }
243              
244 0 0       0 if ($timeout <= 0) {
245 0         0 warn "Pg refused to die gracefully; killing it violently.\n";
246 0         0 kill SIGKILL, $self->pid;
247 0         0 $timeout = 5;
248 0   0     0 while ($timeout > 0 and waitpid($self->pid, WNOHANG) <= 0) {
249 0         0 $timeout -= sleep(1);
250             }
251 0 0       0 if ($timeout <= 0) {
252 0         0 warn "Pg really didn't die.. WTF?\n";
253             }
254             }
255              
256 0         0 $self->pid(undef);
257 0         0 return;
258             }
259              
260             sub setup {
261 0     0 1 0 my $self = shift;
262             # (re)create directory structure
263 0         0 mkdir $self->base_dir;
264 0 0       0 chmod 0755, $self->base_dir
265             or die "failed to chmod 0755 dir:" . $self->base_dir . ":$!";
266 0 0       0 if ($ENV{USER} eq 'root') {
267 0 0       0 chown $self->uid, -1, $self->base_dir
268             or die "failed to chown dir:" . $self->base_dir . ":$!";
269             }
270 0 0       0 if (mkdir $self->base_dir . '/tmp') {
271 0 0       0 if ($self->uid) {
272 0 0       0 chown $self->uid, -1, $self->base_dir . '/tmp'
273             or die "failed to chown dir:" . $self->base_dir . "/tmp:$!";
274             }
275             }
276             # initdb
277 0 0       0 if (! -d $self->base_dir . '/data') {
278 0 0       0 pipe my $rfh, my $wfh
279             or die "failed to create pipe:$!";
280 0         0 my $pid = fork;
281 0 0       0 die "fork failed:$!"
282             unless defined $pid;
283 0 0       0 if ($pid == 0) {
284 0         0 close $rfh;
285 0 0       0 open STDOUT, '>&', $wfh
286             or die "dup(2) failed:$!";
287 0 0       0 open STDERR, '>&', $wfh
288             or die "dup(2) failed:$!";
289 0 0       0 chdir $self->base_dir
290             or die "failed to chdir to:" . $self->base_dir . ":$!";
291 0 0       0 if (defined $self->uid) {
292 0 0       0 setuid($self->uid)
293             or die "setuid failed:$!";
294             }
295 0         0 my $cmd = join(
296             ' ',
297             $self->initdb,
298             $self->initdb_args,
299             '-D', $self->base_dir . '/data',
300             );
301 0         0 exec($cmd);
302 0         0 die "failed to exec:$cmd:$!";
303             }
304 0         0 close $wfh;
305 0         0 my $output = '';
306 0         0 while (my $l = <$rfh>) {
307 0         0 $output .= $l;
308             }
309 0         0 close $rfh;
310 0         0 while (waitpid($pid, 0) <= 0) {
311             }
312 0 0       0 die "*** initdb failed ***\n$output\n"
313             if $? != 0;
314              
315             # use postgres hard-coded configuration as some packagers mess
316             # around with postgresql.conf.sample too much:
317 0         0 truncate $self->base_dir . '/data/postgresql.conf', 0;
318             }
319             }
320              
321             sub _find_program {
322 6     6   11 my $prog = shift;
323 6         13 undef $errstr;
324 6         19 for my $sp (@SEARCH_PATHS) {
325 45 50       420 return "$sp/bin/$prog" if -x "$sp/bin/$prog";
326 45 50       409 return "$sp/$prog" if -x "$sp/$prog";
327             }
328 6         24 $errstr = "could not find $prog, please set appropriate PATH or POSTGRES_HOME";
329 6         41 return;
330             }
331              
332             1;
333             __END__