File Coverage

blib/lib/DBIx/TempDB.pm
Criterion Covered Total %
statement 115 156 73.7
branch 33 72 45.8
condition 17 46 36.9
subroutine 26 31 83.8
pod 7 7 100.0
total 198 312 63.4


line stmt bran cond sub pod time code
1             package DBIx::TempDB;
2 14     14   985065 use strict;
  14         152  
  14         429  
3 14     14   75 use warnings;
  14         26  
  14         446  
4              
5 14     14   75 use Carp qw(confess croak);
  14         25  
  14         710  
6 14     14   78 use Cwd ();
  14         21  
  14         311  
7 14     14   22839 use DBI;
  14         254757  
  14         971  
8 14     14   7262 use DBIx::TempDB::Util qw(dsn_for on_process_end parse_sql);
  14         50  
  14         964  
9 14     14   121 use File::Basename ();
  14         31  
  14         283  
10 14     14   74 use File::Spec;
  14         32  
  14         280  
11 14     14   7940 use IO::Handle ();
  14         89969  
  14         358  
12 14     14   94 use Scalar::Util 'blessed';
  14         33  
  14         668  
13 14     14   7099 use Sys::Hostname ();
  14         14253  
  14         344  
14 14     14   94 use URI::db;
  14         28  
  14         852  
15              
16 14     14   86 use constant CWD => eval { File::Basename::dirname(Cwd::abs_path($0)) };
  14         29  
  14         27  
  14         2881  
17 14   50 14   103 use constant DEBUG => $ENV{DBIX_TEMP_DB_DEBUG} || 0;
  14         28  
  14         826  
18 14   50 14   80 use constant MAX_NUMBER_OF_TRIES => $ENV{DBIX_TEMP_DB_MAX_NUMBER_OF_TRIES} || 20;
  14         28  
  14         17367  
19              
20             our $VERSION = '0.16';
21             our %SCHEMA_DATABASE = (pg => 'postgres', mysql => 'mysql', sqlite => '');
22             my $N = 0;
23              
24             sub create_database {
25 1     1 1 3 my $self = shift;
26 1 50       3 return $self if $self->{created};
27              
28 1         2 local $@;
29 1 0       4 my $mode = !$self->{drop_from_child} ? 'destroy' : $self->{drop_from_child} == 2 ? 'double_fork' : 'fork';
    50          
30 1         3 my ($guard, $name) = 0;
31 1         4 while (++$guard < MAX_NUMBER_OF_TRIES) {
32 1         8 $name = $self->_generate_database_name($N + $guard - 1);
33 1 50       3 eval { $self->_create_database($name) } or next;
  1         3  
34 1         189 $self->{database_name} = $name;
35 1         2 warn "[TempDB:$$] Created @{[$ENV{DBIX_TEMP_DB_KEEP_DATABASE} ? 'permanent' : 'temp']} database $name\n" if DEBUG;
36             $self->{guard} = on_process_end $mode => $self->_drop_database_cb($self->{database_name})
37 1 50       9 unless $ENV{DBIX_TEMP_DB_KEEP_DATABASE};
38 1         3 $self->{created}++;
39 1         11 $self->{url}->dbname($name);
40 1         223 $ENV{DBIX_TEMP_DB_URL} = $self->{url}->uri->as_string;
41 1         31 $N++;
42 1         5 return $self;
43             }
44              
45 0         0 croak qq(Couldn't create database "$name": $@);
46             }
47              
48             sub drop_databases {
49 0     0 1 0 my ($self, $params) = @_;
50              
51 0   0     0 my $self_db_name = $self->{database_name} || '';
52 0   0     0 my $delete_self = $params->{self} || '';
53 0 0       0 delete $self->{guard} if $delete_self;
54              
55             # Drop a single database by name
56 0 0       0 return $self->_drop_database($params->{name}) if $params->{name};
57 0 0       0 return $self->_drop_database($self_db_name) if $delete_self eq 'only';
58              
59             # Drop sibling (and curren) databases
60 0 0       0 my $max = $N > MAX_NUMBER_OF_TRIES ? $N : MAX_NUMBER_OF_TRIES;
61 0         0 my @err;
62 0         0 for my $n (0 .. $max) {
63 0         0 my $name = $self->_generate_database_name($n);
64 0 0 0     0 next unless $delete_self eq 'include' or ($self_db_name and $self_db_name ne $name);
      0        
65 0 0       0 push @err, $@ unless eval { $self->_drop_database($name); 1 };
  0         0  
  0         0  
66             }
67              
68 0 0       0 croak $err[0] if @err == $max;
69 0         0 return $self;
70             }
71              
72             sub dsn {
73 5     5 1 3327 my ($self, $url) = @_;
74              
75 5 50       31 unless (blessed $self) {
76 0         0 Carp::carp("DBIx::TempDB->dsn(...) is deprecated. Use DBIx::TempDB::Util::dsn_for() instead");
77 0 0       0 $url = URI::db->new($url) unless blessed $url;
78 0         0 return dsn_for($url, $url->dbname);
79             }
80              
81 5 50       16 croak "Can't call dsn() before create_database()" unless $self->{database_name};
82 5 50 66     11 croak 'Database does not exist.' if $self->url->canonical_engine eq 'sqlite' and !-e $self->{database_name};
83 5         119 return dsn_for($self->{url}, $self->{database_name});
84             }
85              
86             sub execute {
87 0     0 1 0 my $self = shift;
88 0         0 my $dbh = DBI->connect($self->dsn);
89 0 0       0 local $dbh->{sqlite_allow_multiple_statements} = 1 if $self->url->canonical_engine eq 'sqlite';
90 0         0 $dbh->do($_) for map { parse_sql($self->url, $_) } @_;
  0         0  
91 0         0 return $self;
92             }
93              
94             sub execute_file {
95 1     1 1 17 my ($self, $path) = @_;
96              
97 1 50       9 unless (File::Spec->file_name_is_absolute($path)) {
98 1         3 croak qq(Can't resolve path to "$path".) unless CWD;
99 1         20 $path = File::Spec->catfile(CWD, split '/', $path);
100             }
101              
102 1 50       51 open my $SQL, '<', $path or croak "Can't open $path: $!";
103 1         7 my $ret = my $sql = '';
104 1         14 while ($ret = $SQL->sysread(my $buffer, 131072, 0)) { $sql .= $buffer }
  1         64  
105 1 50       18 croak qq{Can't read "$path": $!} unless defined $ret;
106 1         2 warn "[TempDB:$$] Execute $path\n" if DEBUG;
107 1         4 return $self->execute($sql);
108             }
109              
110             sub new {
111 7     7 1 3093 my $class = shift;
112 7   50     87 my $url = URI::db->new(shift || '');
113 7         7157 my $self = bless {@_, url => $url}, $class;
114              
115 7   100     66 $self->{drop_from_child} //= 1;
116 7   33     102 $self->{schema_database} ||= $SCHEMA_DATABASE{$url->canonical_engine} // croak qq(Unsupported engine for $url);
      66        
117 7   100     342 $self->{template} ||= 'tmp_%U_%X_%H%i';
118 7         9 warn "[TempDB:$$] schema_database=$self->{schema_database}\n" if DEBUG;
119              
120 7 100 100     31 return $self->create_database if $self->{auto_create} // 1;
121 6         34 return $self;
122             }
123              
124 16     16 1 107 sub url { shift->{url}->uri }
125              
126             sub _create_database {
127 1     1   3 my ($self, $name) = @_;
128              
129 1 50       2 if ($self->url->canonical_engine eq 'sqlite') {
130 1         507 require IO::File;
131 14     14   136 use Fcntl qw(O_CREAT O_EXCL O_RDWR);
  14         40  
  14         11980  
132 1 50       2009 IO::File->new->open($name, O_CREAT | O_EXCL | O_RDWR) or confess "Can't write $name: $!\n";
133             }
134             else {
135 0         0 DBI->connect($self->_schema_dsn)->do(sprintf 'create database %s', $name);
136             }
137             }
138              
139 0     0   0 sub _drop_database { shift->_drop_database_cb(shift)->() }
140              
141             sub _drop_database_cb {
142 1     1   4 my ($self, $name) = @_;
143              
144 1 50       4 if ($self->url->canonical_engine eq 'sqlite') {
145             return sub {
146 1     1   15 local $! = 0;
147 1 50       76 unlink $name if -e $name;
148 1 50 33     8 confess "[TempDB:$$] Can't unlink $name: $!" if $! and $! != 2;
149 1         18 warn "[TempDB:$$] Dropped temp database $name\n" if DEBUG;
150 1         31 };
151             }
152              
153 0         0 my $sql = sprintf 'drop database if exists %s', $name;
154 0         0 $sql =~ s!\%d!$name!g;
155             return sub {
156 0     0   0 my $dbh = DBI->connect($self->_schema_dsn);
157 0         0 eval { $dbh->do('set client_min_messages to warning') }; # for postgres
  0         0  
158 0         0 $dbh->do($sql);
159 0         0 warn "[TempDB:$$] Dropped temp database $name\n" if DEBUG;
160 0         0 };
161             }
162              
163             sub _generate_database_name {
164 6     6   2639 my ($self, $n) = @_;
165 6         12 my $name = $self->{template};
166              
167 6         32 $name =~ s/\%([iHPTUX])/{
168 20 100       45 $1 eq 'i' ? ($n > 0 ? "_$n" : '')
  20 50       224  
    100          
    100          
    100          
    100          
    100          
169             : $1 eq 'H' ? Sys::Hostname::hostname()
170             : $1 eq 'P' ? $$
171             : $1 eq 'T' ? $^T
172             : $1 eq 'U' ? $<
173             : $1 eq 'X' ? File::Basename::basename($0)
174             : "\%$1"
175             }/egx;
176              
177 6 50 33     24 if (63 < length $name and !$self->{keep_too_long_database_name}) {
178             confess qq(Can't create a shorter database name with "$self->{template}".)
179             unless $self->{template} =~ s!\%T!!g
180             or $self->{template} =~ s!\%H!!g
181 0 0 0     0 or $self->{template} =~ s!\%X!!g;
      0        
182 0         0 return $self->_generate_database_name($n);
183             }
184              
185 6         12 $name =~ s!^/+!!;
186 6         24 $name =~ s!\W!_!g;
187 6         15 $name = lc $name;
188              
189 6 100       18 return $name if $self->url->canonical_engine ne 'sqlite';
190 1         22 return File::Spec->catfile($self->_tempdir, "$name.sqlite");
191             }
192              
193             sub _schema_dsn {
194 0     0   0 my $self = shift;
195 0         0 local $self->{database_name} = $self->{schema_database};
196 0         0 return $self->dsn;
197             }
198              
199             sub _tempdir {
200 1   33 1   27 shift->{tempdir} ||= File::Spec->tmpdir;
201             }
202              
203             1;
204              
205             =encoding utf8
206              
207             =head1 NAME
208              
209             DBIx::TempDB - Create a temporary database
210              
211             =head1 VERSION
212              
213             0.16
214              
215             =head1 SYNOPSIS
216              
217             use Test::More;
218             use DBIx::TempDB;
219             use DBI;
220              
221             # provide credentials with environment variables
222             plan skip_all => 'TEST_PG_DSN=postgresql://postgres@localhost' unless $ENV{TEST_PG_DSN};
223              
224             # create a temp database
225             my $tmpdb = DBIx::TempDB->new($ENV{TEST_PG_DSN});
226              
227             # print complete url to db server with database name
228             diag $tmpdb->url;
229              
230             # useful for reading in fixtures
231             $tmpdb->execute("create table users (name text)");
232             $tmpdb->execute_file("path/to/file.sql");
233              
234             # connect to the temp database
235             my $db = DBI->connect($tmpdb->dsn);
236              
237             # run tests...
238              
239             done_testing;
240             # database is cleaned up when test exit
241              
242             =head1 DESCRIPTION
243              
244             L is a module which allows you to create a temporary database,
245             which only lives as long as your process is alive. This can be very
246             convenient when you want to run tests in parallel, without messing up the
247             state between tests.
248              
249             This module currently support PostgreSQL, MySQL and SQLite by installing the optional
250             L, L and/or L modules.
251              
252             Please create an L
253             or pull request for more backend support.
254              
255             =head1 CAVEAT
256              
257             Creating a database is easy, but making sure it gets clean up when your
258             process exit is a totally different ball game. This means that
259             L might fill up your server with random databases, unless
260             you choose the right "drop strategy". Have a look at the L
261             parameter you can give to L and test the different values and select
262             the one that works for you.
263              
264             =head1 ENVIRONMENT VARIABLES
265              
266             =head2 DBIX_TEMP_DB_KEEP_DATABASE
267              
268             Setting this variable will disable the core feature in this module:
269             A unique database will be created, but it will not get dropped/deleted.
270              
271             =head2 DBIX_TEMP_DB_URL
272              
273             This variable is set by L and contains the complete
274             URL pointing to the temporary database.
275              
276             Note that calling L on different instances of
277             L will overwrite C.
278              
279             =head1 METHODS
280              
281             =head2 create_database
282              
283             $tmpdb = $tmpdb->create_database;
284              
285             This method will create a temp database for the current process. Calling this
286             method multiple times will simply do nothing. This method is normally
287             automatically called by L.
288              
289             The database name generate is defined by the L parameter passed to
290             L, but normalization will be done to make it work for the given database.
291              
292             =head2 drop_databases
293              
294             $tmpdb->drop_databases;
295             $tmpdb->drop_databases({tmpdb => "include"});
296             $tmpdb->drop_databases({tmpdb => "only"});
297             $tmpdb->drop_databases({name => "some_database_name"});
298              
299             Used to drop either sibling databases (default), sibling databases and the
300             current database or a given database by name.
301              
302             =head2 dsn
303              
304             ($dsn, $user, $pass, $attrs) = $tmpdb->dsn;
305              
306             Will parse L and return a list of arguments suitable for L.
307              
308             Note that this method cannot be called as an object method before
309             L is called.
310              
311             See also L.
312              
313             =head2 execute
314              
315             $tmpdb = $tmpdb->execute(@sql);
316              
317             This method will execute a list of C<@sql> statements in the temporary
318             SQL server.
319              
320             =head2 execute_file
321              
322             $tmpdb = $tmpdb->execute_file("relative/to/executable.sql");
323             $tmpdb = $tmpdb->execute_file("/absolute/path/stmt.sql");
324              
325             This method will read the contents of a file and execute the SQL statements
326             in the temporary server.
327              
328             This method is a thin wrapper around L.
329              
330             =head2 new
331              
332             $tmpdb = DBIx::TempDB->new($url, %args);
333             $tmpdb = DBIx::TempDB->new("mysql://127.0.0.1");
334             $tmpdb = DBIx::TempDB->new("postgresql://postgres@db.example.com");
335             $tmpdb = DBIx::TempDB->new("sqlite:");
336              
337             Creates a new object after checking the C<$url> is valid. C<%args> can be:
338              
339             =over 4
340              
341             =item * auto_create
342              
343             L will be called automatically, unless C is
344             set to a false value.
345              
346             =item * drop_from_child
347              
348             Setting "drop_from_child" to a true value will create a child process which
349             will remove the temporary database, when the main process ends. There are two
350             possible values:
351              
352             C (the default) will create a child process which monitor
353             the L object with a pipe. This will then DROP the temp database
354             if the object goes out of scope or if the process ends.
355              
356             C will create a child process detached from the parent,
357             which monitor the parent with C.
358              
359             The double fork code is based on a paste contributed by
360             L, Knut Arne Bjørndal.
361              
362             See also L.
363              
364             =item * template
365              
366             Customize the generated database name. Default template is "tmp_%U_%X_%H%i".
367             Possible variables to expand are:
368              
369             %i = The number of tries if tries are higher than 0. Example: "_3"
370             %H = Hostname
371             %P = Process ID ($$)
372             %T = Process start time ($^T)
373             %U = UID of current user
374             %X = Basename of executable
375              
376             The default is subject to change!
377              
378             =back
379              
380             =head2 url
381              
382             $url = $tmpdb->url;
383              
384             Returns the input URL as L compatible object. This URL will have
385             the L part set to the database from L,
386             but not I after L is actually called.
387              
388             The URL returned can be passed directly to modules such as L
389             and L.
390              
391             =head1 COPYRIGHT AND LICENSE
392              
393             Copyright (C) 2015, Jan Henning Thorsen
394              
395             This program is free software, you can redistribute it and/or modify it under
396             the terms of the Artistic License version 2.0.
397              
398             =head1 AUTHOR
399              
400             Jan Henning Thorsen - C
401              
402             =cut