File Coverage

blib/lib/Test/TempDatabase.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1 5     5   390219 use strict;
  5         13  
  5         369  
2 5     5   31 use warnings FATAL => 'all';
  5         11  
  5         376  
3              
4             package Test::TempDatabase;
5              
6             our $VERSION = 0.16;
7 5     5   14564 use DBI;
  5         135315  
  5         450  
8 5     5   3287 use DBD::Pg;
  0            
  0            
9             use POSIX qw(setuid);
10             use Carp;
11             use File::Slurp;
12              
13             =head1 NAME
14              
15             Test::TempDatabase - temporary database creation and destruction.
16              
17             =head1 SYNOPSIS
18              
19             use Test::TempDatabase;
20            
21             my $td = Test::TempDatabase->create(dbname => 'temp_db');
22             my $dbh = $td->handle;
23              
24             ... some tests ...
25             # Test::TempDatabase drops database
26              
27             =head1 DESCRIPTION
28              
29             This module automates creation and dropping of test databases.
30              
31             =head1 USAGE
32              
33             Create test database using Test::TempDatabase->create. Use C
34             to get a handle to the database. Database will be automagically dropped
35             when Test::TempDatabase instance goes out of scope.
36              
37             =cut
38             sub connect {
39             my ($self, $db_name) = @_;
40             my $cp = $self->connect_params;
41             $db_name ||= $cp->{dbname};
42             my $h = $cp->{cluster_dir} ? "host=$cp->{cluster_dir};" : "";
43             my $dbi_args = $cp->{dbi_args} || { RaiseError => 1, AutoCommit => 1 };
44             return DBI->connect("dbi:Pg:dbname=$db_name;$h" . ($cp->{rest} || ''),
45             $cp->{username}, $cp->{password}, $dbi_args);
46             }
47              
48             sub find_postgres_user {
49             return $< if $<;
50              
51             my $uname = $ENV{TEST_TEMP_DB_USER} || $ENV{SUDO_USER} || "postgres";
52             return getpwnam($uname);
53             }
54              
55             =head2 $class->become_postgres_user
56              
57             When running as root, this function becomes different user.
58             It decides on the user name by probing TEST_TEMP_DB_USER, SUDO_USER environment
59             variables. If these variables are empty, default "postgres" user is used.
60              
61             =cut
62             sub become_postgres_user {
63             my $class = shift;
64             return if $<;
65              
66             my $p_uid = $class->find_postgres_user;
67             my @pw = getpwuid($p_uid);
68              
69             carp("# $class\->become_postgres_user: setting $pw[0] uid\n");
70             setuid($p_uid) or die "Unable to set $p_uid uid";
71             $ENV{HOME} = $pw[ $#pw - 1 ];
72             }
73              
74             sub create_db {
75             my $self = shift;
76             my $cp = $self->connect_params;
77             my $dbh = $self->connect('template1');
78              
79             my $found = @{ $dbh->selectcol_arrayref(
80             "select datname from pg_database where "
81             . "datname = '$cp->{dbname}'") };
82              
83             my $drop_it = (!$cp->{no_drop} && $found);
84             $self->drop_db if $drop_it;
85              
86             my $tn = $cp->{template} ? "template \"$cp->{template}\"" : "";
87             $dbh->do("create database \"$cp->{dbname}\" $tn")
88             if ($drop_it || !$found);
89             $dbh->disconnect;
90             $dbh = $self->connect($cp->{dbname});
91             $self->{db_handle} = $dbh;
92              
93             if (my $schema = $cp->{schema}) {
94             my $vs = $schema->new($dbh);
95             $vs->run_updates;
96             $self->{schema} = $vs;
97             }
98             }
99              
100             =head2 create
101              
102             Creates temporary database. It will be dropped when the resulting
103             instance will go out of scope.
104              
105             Arguments are passed in as a keyword-value pairs. Available keywords are:
106              
107             dbname: the name of the temporary database.
108              
109             rest: the rest of the database connection string. It can be used to connect to
110             a different host, etc.
111              
112             username, password: self-explanatory.
113              
114             =cut
115             sub create {
116             my ($class, %args) = @_;
117             my $self = $class->new(\%args);
118             $self->become_postgres_user;
119             $self->create_db;
120             return $self;
121             }
122              
123             sub new {
124             my ($class, $args) = @_;
125             my $self = bless { connect_params => $args }, $class;
126             $self->{pid} = $$;
127             return $self;
128             }
129              
130             sub _call_pg_cmd {
131             my ($self, $cmd) = @_;
132             my ($bdir) = (`pg_config | grep BINDIR` =~ /= (\S+)$/);
133             $cmd = "$bdir/$cmd";
134             $cmd = "su - postgres -c '$cmd'" unless $<;
135             my $res = `$cmd 2>&1`;
136             confess $res if $?;
137             }
138              
139             sub create_cluster {
140             my $self = shift;
141             my $cdir = $self->{connect_params}->{cluster_dir};
142             $self->_call_pg_cmd("initdb -D $cdir");
143             append_file("$cdir/postgresql.conf"
144             , "\nlisten_addresses = ''\nunix_socket_directory = '$cdir'\n");
145             }
146              
147             sub start_server {
148             my $self = shift;
149             my $cdir = $self->{connect_params}->{cluster_dir};
150             $self->_call_pg_cmd("pg_ctl -D $cdir -l $cdir/log start");
151              
152             sleep 1;
153             for (1 .. 5) {
154             my $log = read_file("$cdir/log");
155             return if $log =~ /ready to accept/;
156             sleep 1;
157             }
158             die "Server did not start " . read_file("$cdir/log");
159             }
160              
161             sub stop_server {
162             my $self = shift;
163             my $cdir = $self->{connect_params}->{cluster_dir};
164             $self->_call_pg_cmd("pg_ctl -D $cdir -m fast -l $cdir/log stop");
165             }
166              
167             sub connect_params { return shift()->{connect_params}; }
168             sub handle { return shift()->{db_handle}; }
169              
170             sub drop_db {
171             my $self = shift;
172             my $dn = $self->connect_params->{dbname};
173             my @plines = `ps auxx | grep post | grep $dn | grep -v grep`;
174             my $dbh = $self->connect('template1');
175             for (@plines) {
176             /\w\s+(\d+)/ or next;
177             $dbh->do("select pg_terminate_backend($1)");
178             }
179             $dbh->do(q{ set client_min_messages to warning });
180             $dbh->do("drop database if exists \"$dn\"");
181             $dbh->disconnect;
182             $self->{db_handle} = undef;
183             }
184              
185             sub destroy {
186             my $self = shift;
187             return if $self->handle->{InactiveDestroy};
188             $self->handle->disconnect;
189             $self->{db_handle} = undef;
190             return unless $self->{pid} == $$;
191             return if $self->connect_params->{no_drop};
192             $self->drop_db;
193             }
194              
195             sub DESTROY {
196             my $self = shift;
197             $self->destroy if $self->handle;
198             }
199              
200             sub dump_db {
201             my ($self, $file) = @_;
202             my $cp = $self->connect_params;
203             my $h = $cp->{cluster_dir} ? "-h $cp->{cluster_dir}" : "";
204             my $cmd = "pg_dump $h -O -c $cp->{dbname} > $file";
205             system($cmd) and confess "Unable to do $cmd";
206             }
207              
208             =head1 BUGS
209              
210             * Works with PostgreSQL database currently.
211              
212             =head1 AUTHOR
213              
214             Boris Sukholitko
215             boriss@gmail.com
216              
217             =head1 COPYRIGHT
218              
219             This program is free software; you can redistribute
220             it and/or modify it under the same terms as Perl itself.
221              
222             The full text of the license can be found in the
223             LICENSE file included with this module.
224              
225              
226             =head1 SEE ALSO
227              
228             Test::More
229              
230             =cut
231              
232             1;