Better handling of bogus tmpdir returned by File::Spec on MSWin32
[dbsrgits/DBIx-Class.git] / t / lib / DBICTest.pm
1 package # hide from PAUSE
2     DBICTest;
3
4 use strict;
5 use warnings;
6 use DBICTest::RunMode;
7 use DBICTest::Schema;
8 use DBICTest::Util qw/populate_weakregistry assert_empty_weakregistry local_umask/;
9 use Carp;
10 use Path::Class::File ();
11 use File::Spec;
12 use Fcntl qw/:flock/;
13
14 =head1 NAME
15
16 DBICTest - Library to be used by DBIx::Class test scripts.
17
18 =head1 SYNOPSIS
19
20   use lib qw(t/lib);
21   use DBICTest;
22   use Test::More;
23
24   my $schema = DBICTest->init_schema();
25
26 =head1 DESCRIPTION
27
28 This module provides the basic utilities to write tests against
29 DBIx::Class.
30
31 =head1 METHODS
32
33 =head2 init_schema
34
35   my $schema = DBICTest->init_schema(
36     no_deploy=>1,
37     no_populate=>1,
38     storage_type=>'::DBI::Replicated',
39     storage_type_args=>{
40       balancer_type=>'DBIx::Class::Storage::DBI::Replicated::Balancer::Random'
41     },
42   );
43
44 This method removes the test SQLite database in t/var/DBIxClass.db
45 and then creates a new, empty database.
46
47 This method will call deploy_schema() by default, unless the
48 no_deploy flag is set.
49
50 Also, by default, this method will call populate_schema() by
51 default, unless the no_deploy or no_populate flags are set.
52
53 =cut
54
55 # some tests are very time sensitive and need to run on their own, without
56 # being disturbed by anything else grabbing CPU or disk IO. Hence why everything
57 # using DBICTest grabs a shared lock, and the few tests that request a :GlobalLock
58 # will ask for an exclusive one and block until they can get it
59 our ($global_lock_fh, $global_exclusive_lock);
60 sub import {
61     my $self = shift;
62
63     my $lockpath = DBICTest::RunMode->tmpdir->file('.dbictest_global.lock');
64
65     {
66       my $u = local_umask(0); # so that the file opens as 666, and any user can lock
67       open ($global_lock_fh, '>', $lockpath)
68         or die "Unable to open $lockpath: $!";
69     }
70
71     for (@_) {
72         if ($_ eq ':GlobalLock') {
73             flock ($global_lock_fh, LOCK_EX) or die "Unable to lock $lockpath: $!";
74             $global_exclusive_lock = 1;
75         }
76         else {
77             croak "Unknown export $_ requested from $self";
78         }
79     }
80
81     unless ($global_exclusive_lock) {
82         flock ($global_lock_fh, LOCK_SH) or die "Unable to lock $lockpath: $!";
83     }
84 }
85
86 END {
87     if ($global_lock_fh) {
88         # delay destruction even more
89     }
90 }
91
92 {
93     my $dir = Path::Class::File->new(__FILE__)->dir->parent->subdir('var');
94     $dir->mkpath unless -d "$dir";
95     $dir = "$dir";
96
97     sub _sqlite_dbfilename {
98         my $holder = $ENV{DBICTEST_LOCK_HOLDER} || $$;
99         $holder = $$ if $holder == -1;
100
101         # useful for missing cleanup debugging
102         #if ( $holder == $$) {
103         #  my $x = $0;
104         #  $x =~ s/\//#/g;
105         #  $holder .= "-$x";
106         #}
107
108         return "$dir/DBIxClass-$holder.db";
109     }
110
111     END {
112         _cleanup_dbfile();
113     }
114 }
115
116 $SIG{INT} = sub { _cleanup_dbfile(); exit 1 };
117
118 sub _cleanup_dbfile {
119     # cleanup if this is us
120     if (
121       ! $ENV{DBICTEST_LOCK_HOLDER}
122         or
123       $ENV{DBICTEST_LOCK_HOLDER} == -1
124         or
125       $ENV{DBICTEST_LOCK_HOLDER} == $$
126     ) {
127         my $db_file = _sqlite_dbfilename();
128         unlink $_ for ($db_file, "${db_file}-journal");
129     }
130 }
131
132 sub has_custom_dsn {
133     return $ENV{"DBICTEST_DSN"} ? 1:0;
134 }
135
136 sub _sqlite_dbname {
137     my $self = shift;
138     my %args = @_;
139     return $self->_sqlite_dbfilename if (
140       defined $args{sqlite_use_file} ? $args{sqlite_use_file} : $ENV{'DBICTEST_SQLITE_USE_FILE'}
141     );
142     return ":memory:";
143 }
144
145 sub _database {
146     my $self = shift;
147     my %args = @_;
148
149     if ($ENV{DBICTEST_DSN}) {
150       return (
151         (map { $ENV{"DBICTEST_${_}"} || '' } qw/DSN DBUSER DBPASS/),
152         { AutoCommit => 1, %args },
153       );
154     }
155     my $db_file = $self->_sqlite_dbname(%args);
156
157     for ($db_file, "${db_file}-journal") {
158       next unless -e $_;
159       unlink ($_) or carp (
160         "Unable to unlink existing test database file $_ ($!), creation of fresh database / further tests may fail!"
161       );
162     }
163
164     return ("dbi:SQLite:${db_file}", '', '', {
165       AutoCommit => 1,
166
167       # this is executed on every connect, and thus installs a disconnect/DESTROY
168       # guard for every new $dbh
169       on_connect_do => sub {
170         my $storage = shift;
171         my $dbh = $storage->_get_dbh;
172
173         # no fsync on commit
174         $dbh->do ('PRAGMA synchronous = OFF');
175
176         # set a *DBI* disconnect callback, to make sure the physical SQLite
177         # file is still there (i.e. the test does not attempt to delete
178         # an open database, which fails on Win32)
179         if (my $guard_cb = __mk_disconnect_guard($db_file)) {
180           $dbh->{Callbacks} = {
181             connect => sub { $guard_cb->('connect') },
182             disconnect => sub { $guard_cb->('disconnect') },
183             DESTROY => sub { $guard_cb->('DESTROY') },
184           };
185         }
186       },
187       %args,
188     });
189 }
190
191 sub __mk_disconnect_guard {
192   return if DBIx::Class::_ENV_::PEEPEENESS(); # leaks handles, delaying DESTROY, can't work right
193
194   my $db_file = shift;
195   return unless -f $db_file;
196
197   my $orig_inode = (stat($db_file))[1]
198     or return;
199
200   my $clan_connect_caller = '*UNKNOWN*';
201   my $i;
202   while ( my ($pack, $file, $line) = caller(++$i) ) {
203     next if $file eq __FILE__;
204     next if $pack =~ /^DBIx::Class|^Try::Tiny/;
205     $clan_connect_caller = "$file line $line";
206   }
207
208   my $failed_once = 0;
209   my $connected = 1;
210
211   return sub {
212     return if $failed_once;
213
214     my $event = shift;
215     if ($event eq 'connect') {
216       # this is necessary in case we are disconnected and connected again, all within the same $dbh object
217       $connected = 1;
218       return;
219     }
220     elsif ($event eq 'disconnect') {
221       $connected = 0;
222     }
223     elsif ($event eq 'DESTROY' and ! $connected ) {
224       return;
225     }
226
227     my $fail_reason;
228     if (! -e $db_file) {
229       $fail_reason = 'is missing';
230     }
231     else {
232       my $cur_inode = (stat($db_file))[1];
233
234       if ($orig_inode != $cur_inode) {
235         # pack/unpack to match the unsigned longs returned by `stat`
236         $fail_reason = sprintf 'was recreated (initially inode %s, now %s)', (
237           map { unpack ('L', pack ('l', $_) ) } ($orig_inode, $cur_inode )
238         );
239       }
240     }
241
242     if ($fail_reason) {
243       $failed_once++;
244
245       require Test::Builder;
246       my $t = Test::Builder->new;
247       local $Test::Builder::Level = $Test::Builder::Level + 3;
248       $t->ok (0,
249         "$db_file originally created at $clan_connect_caller $fail_reason before $event "
250       . 'of DBI handle - a strong indicator that the database file was tampered with while '
251       . 'still being open. This action would fail massively if running under Win32, hence '
252       . 'we make sure it fails on any OS :)'
253       );
254     }
255
256     return; # this empty return is a DBI requirement
257   };
258 }
259
260 my $weak_registry = {};
261
262 sub init_schema {
263     my $self = shift;
264     my %args = @_;
265
266     my $schema;
267
268     if ($args{compose_connection}) {
269       $schema = DBICTest::Schema->compose_connection(
270                   'DBICTest', $self->_database(%args)
271                 );
272     } else {
273       $schema = DBICTest::Schema->compose_namespace('DBICTest');
274     }
275
276     if( $args{storage_type}) {
277       $schema->storage_type($args{storage_type});
278     }
279
280     if ( !$args{no_connect} ) {
281       $schema = $schema->connect($self->_database(%args));
282     }
283
284     if ( !$args{no_deploy} ) {
285         __PACKAGE__->deploy_schema( $schema, $args{deploy_args} );
286         __PACKAGE__->populate_schema( $schema )
287          if( !$args{no_populate} );
288     }
289
290     populate_weakregistry ( $weak_registry, $schema->storage )
291       if $INC{'Test/Builder.pm'} and $schema->storage;
292
293     return $schema;
294 }
295
296 END {
297     assert_empty_weakregistry($weak_registry, 'quiet');
298 }
299
300 =head2 deploy_schema
301
302   DBICTest->deploy_schema( $schema );
303
304 This method does one of two things to the schema.  It can either call
305 the experimental $schema->deploy() if the DBICTEST_SQLT_DEPLOY environment
306 variable is set, otherwise the default is to read in the t/lib/sqlite.sql
307 file and execute the SQL within. Either way you end up with a fresh set
308 of tables for testing.
309
310 =cut
311
312 sub deploy_schema {
313     my $self = shift;
314     my $schema = shift;
315     my $args = shift || {};
316
317     if ($ENV{"DBICTEST_SQLT_DEPLOY"}) {
318         $schema->deploy($args);
319     } else {
320         my $filename = Path::Class::File->new(__FILE__)->dir
321           ->file('sqlite.sql')->stringify;
322         my $sql = do { local (@ARGV, $/) = $filename ; <> };
323         for my $chunk ( split (/;\s*\n+/, $sql) ) {
324           if ( $chunk =~ / ^ (?! --\s* ) \S /xm ) {  # there is some real sql in the chunk - a non-space at the start of the string which is not a comment
325             $schema->storage->dbh_do(sub { $_[1]->do($chunk) }) or print "Error on SQL: $chunk\n";
326           }
327         }
328     }
329     return;
330 }
331
332 =head2 populate_schema
333
334   DBICTest->populate_schema( $schema );
335
336 After you deploy your schema you can use this method to populate
337 the tables with test data.
338
339 =cut
340
341 sub populate_schema {
342     my $self = shift;
343     my $schema = shift;
344
345     $schema->populate('Genre', [
346       [qw/genreid name/],
347       [qw/1       emo  /],
348     ]);
349
350     $schema->populate('Artist', [
351         [ qw/artistid name/ ],
352         [ 1, 'Caterwauler McCrae' ],
353         [ 2, 'Random Boy Band' ],
354         [ 3, 'We Are Goth' ],
355     ]);
356
357     $schema->populate('CD', [
358         [ qw/cdid artist title year genreid/ ],
359         [ 1, 1, "Spoonful of bees", 1999, 1 ],
360         [ 2, 1, "Forkful of bees", 2001 ],
361         [ 3, 1, "Caterwaulin' Blues", 1997 ],
362         [ 4, 2, "Generic Manufactured Singles", 2001 ],
363         [ 5, 3, "Come Be Depressed With Us", 1998 ],
364     ]);
365
366     $schema->populate('LinerNotes', [
367         [ qw/liner_id notes/ ],
368         [ 2, "Buy Whiskey!" ],
369         [ 4, "Buy Merch!" ],
370         [ 5, "Kill Yourself!" ],
371     ]);
372
373     $schema->populate('Tag', [
374         [ qw/tagid cd tag/ ],
375         [ 1, 1, "Blue" ],
376         [ 2, 2, "Blue" ],
377         [ 3, 3, "Blue" ],
378         [ 4, 5, "Blue" ],
379         [ 5, 2, "Cheesy" ],
380         [ 6, 4, "Cheesy" ],
381         [ 7, 5, "Cheesy" ],
382         [ 8, 2, "Shiny" ],
383         [ 9, 4, "Shiny" ],
384     ]);
385
386     $schema->populate('TwoKeys', [
387         [ qw/artist cd/ ],
388         [ 1, 1 ],
389         [ 1, 2 ],
390         [ 2, 2 ],
391     ]);
392
393     $schema->populate('FourKeys', [
394         [ qw/foo bar hello goodbye sensors/ ],
395         [ 1, 2, 3, 4, 'online' ],
396         [ 5, 4, 3, 6, 'offline' ],
397     ]);
398
399     $schema->populate('OneKey', [
400         [ qw/id artist cd/ ],
401         [ 1, 1, 1 ],
402         [ 2, 1, 2 ],
403         [ 3, 2, 2 ],
404     ]);
405
406     $schema->populate('SelfRef', [
407         [ qw/id name/ ],
408         [ 1, 'First' ],
409         [ 2, 'Second' ],
410     ]);
411
412     $schema->populate('SelfRefAlias', [
413         [ qw/self_ref alias/ ],
414         [ 1, 2 ]
415     ]);
416
417     $schema->populate('ArtistUndirectedMap', [
418         [ qw/id1 id2/ ],
419         [ 1, 2 ]
420     ]);
421
422     $schema->populate('Producer', [
423         [ qw/producerid name/ ],
424         [ 1, 'Matt S Trout' ],
425         [ 2, 'Bob The Builder' ],
426         [ 3, 'Fred The Phenotype' ],
427     ]);
428
429     $schema->populate('CD_to_Producer', [
430         [ qw/cd producer/ ],
431         [ 1, 1 ],
432         [ 1, 2 ],
433         [ 1, 3 ],
434     ]);
435
436     $schema->populate('TreeLike', [
437         [ qw/id parent name/ ],
438         [ 1, undef, 'root' ],
439         [ 2, 1, 'foo'  ],
440         [ 3, 2, 'bar'  ],
441         [ 6, 2, 'blop' ],
442         [ 4, 3, 'baz'  ],
443         [ 5, 4, 'quux' ],
444         [ 7, 3, 'fong'  ],
445     ]);
446
447     $schema->populate('Track', [
448         [ qw/trackid cd  position title/ ],
449         [ 4, 2, 1, "Stung with Success"],
450         [ 5, 2, 2, "Stripy"],
451         [ 6, 2, 3, "Sticky Honey"],
452         [ 7, 3, 1, "Yowlin"],
453         [ 8, 3, 2, "Howlin"],
454         [ 9, 3, 3, "Fowlin"],
455         [ 10, 4, 1, "Boring Name"],
456         [ 11, 4, 2, "Boring Song"],
457         [ 12, 4, 3, "No More Ideas"],
458         [ 13, 5, 1, "Sad"],
459         [ 14, 5, 2, "Under The Weather"],
460         [ 15, 5, 3, "Suicidal"],
461         [ 16, 1, 1, "The Bees Knees"],
462         [ 17, 1, 2, "Apiary"],
463         [ 18, 1, 3, "Beehind You"],
464     ]);
465
466     $schema->populate('Event', [
467         [ qw/id starts_at created_on varchar_date varchar_datetime skip_inflation/ ],
468         [ 1, '2006-04-25 22:24:33', '2006-06-22 21:00:05', '2006-07-23', '2006-05-22 19:05:07', '2006-04-21 18:04:06'],
469     ]);
470
471     $schema->populate('Link', [
472         [ qw/id url title/ ],
473         [ 1, '', 'aaa' ]
474     ]);
475
476     $schema->populate('Bookmark', [
477         [ qw/id link/ ],
478         [ 1, 1 ]
479     ]);
480
481     $schema->populate('Collection', [
482         [ qw/collectionid name/ ],
483         [ 1, "Tools" ],
484         [ 2, "Body Parts" ],
485     ]);
486
487     $schema->populate('TypedObject', [
488         [ qw/objectid type value/ ],
489         [ 1, "pointy", "Awl" ],
490         [ 2, "round", "Bearing" ],
491         [ 3, "pointy", "Knife" ],
492         [ 4, "pointy", "Tooth" ],
493         [ 5, "round", "Head" ],
494     ]);
495     $schema->populate('CollectionObject', [
496         [ qw/collection object/ ],
497         [ 1, 1 ],
498         [ 1, 2 ],
499         [ 1, 3 ],
500         [ 2, 4 ],
501         [ 2, 5 ],
502     ]);
503
504     $schema->populate('Owners', [
505         [ qw/id name/ ],
506         [ 1, "Newton" ],
507         [ 2, "Waltham" ],
508     ]);
509
510     $schema->populate('BooksInLibrary', [
511         [ qw/id owner title source price/ ],
512         [ 1, 1, "Programming Perl", "Library", 23 ],
513         [ 2, 1, "Dynamical Systems", "Library",  37 ],
514         [ 3, 2, "Best Recipe Cookbook", "Library", 65 ],
515     ]);
516 }
517
518 1;