summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicholas Bamber <nicholas@periapt.co.uk>2010-11-26 22:08:43 +0000
committerNicholas Bamber <nicholas@periapt.co.uk>2010-11-26 22:08:43 +0000
commit9e62fccfaba9a6c64846b86f911d6286a490fddc (patch)
tree8a7ef90d1a5fd8446a48cf80e89c87032de43553
[svn-inject] Installing original source of libtest-database-perl (1.11)
-rw-r--r--Build.PL31
-rw-r--r--Changes145
-rw-r--r--MANIFEST36
-rw-r--r--META.yml46
-rw-r--r--Makefile.PL20
-rw-r--r--README58
-rw-r--r--eg/MyDriver.pm84
-rw-r--r--lib/Test/Database.pm475
-rw-r--r--lib/Test/Database/Driver.pm527
-rw-r--r--lib/Test/Database/Driver/CSV.pm63
-rw-r--r--lib/Test/Database/Driver/DBM.pm64
-rw-r--r--lib/Test/Database/Driver/Pg.pm100
-rw-r--r--lib/Test/Database/Driver/SQLite.pm62
-rw-r--r--lib/Test/Database/Driver/SQLite2.pm62
-rw-r--r--lib/Test/Database/Driver/mysql.pm90
-rw-r--r--lib/Test/Database/Handle.pm147
-rw-r--r--lib/Test/Database/Tutorial.pod367
-rw-r--r--lib/Test/Database/Util.pm98
-rw-r--r--t/00-load.t16
-rw-r--r--t/08-handle.t51
-rw-r--r--t/09-handle-dsn.t45
-rw-r--r--t/10-drivers.t106
-rw-r--r--t/10-list_drivers.t33
-rw-r--r--t/11-available_dbname.t44
-rw-r--r--t/11-make_dsn.t40
-rw-r--r--t/11-version_matches.t88
-rw-r--r--t/12-load.t56
-rw-r--r--t/20-handles.t106
-rw-r--r--t/25-sql.t86
-rw-r--r--t/database.bad14
-rw-r--r--t/database.bad23
-rw-r--r--t/database.empty4
-rw-r--r--t/database.good17
-rw-r--r--t/database.rc14
-rw-r--r--t/pod-coverage.t40
-rw-r--r--t/pod.t12
36 files changed, 3250 insertions, 0 deletions
diff --git a/Build.PL b/Build.PL
new file mode 100644
index 0000000..9922456
--- /dev/null
+++ b/Build.PL
@@ -0,0 +1,31 @@
+use 5.006;
+use strict;
+use warnings;
+use Module::Build;
+
+my $builder = Module::Build->new(
+ module_name => 'Test::Database',
+ license => 'perl',
+ dist_author => 'Philippe Bruhat (BooK) <book@cpan.org>',
+ dist_version_from => 'lib/Test/Database.pm',
+ requires => {
+ 'DBI' => 1,
+ 'File::HomeDir' => 0.50,
+ 'version' => 0,
+ 'YAML::Tiny' => 1.27,
+ 'File::Spec' => 0,
+ 'File::Path' => 0,
+ 'perl' => 5.006,
+ },
+ build_requires => {
+ 'Test::More' => 0,
+ },
+ meta_merge => {
+ resources => {
+ repository => 'http://github.com/book/Test-Database',
+ },
+ },
+ add_to_cleanup => [ 'Test-Database-*' ],
+);
+
+$builder->create_build_script();
diff --git a/Changes b/Changes
new file mode 100644
index 0000000..308acdc
--- /dev/null
+++ b/Changes
@@ -0,0 +1,145 @@
+Revision history for Test-Database
+
+1.11 Tue May 4 01:03:10 CEST 2010
+ [IMPROVEMENTS]
+ - new version_string() method ensures version constraints on
+ requests work correctly (thanks to Erik Rijkers)
+ - new paramater for requests: regex_version
+ [TESTS]
+ - workaround for DBD::DBM errors in t/25-sql.t, thanks to
+ Birmingham.pm
+
+1.10 Tue Apr 27 00:58:22 CEST 2010
+ [IMPROVEMENTS]
+ - Do not print errors when errors are expected and will be ignored.
+ (Thanks to Barbie - RT #56516)
+ [TESTS]
+ - clean t/10-drivers.t of warnings and errors
+ (Thanks to Barbie & Martin J Evans - RT #56516)
+
+1.09 Tue Mar 16 12:43:08 CET 2010
+ [TESTS]
+ - added t/24-cleanup.t to ensure all databases used by the test suite
+ [DOCUMENTATION]
+ - fixed copyright dates, added an author/license section to the
+ tutorial
+
+1.08 Mon Mar 15 15:00:45 CET 2010
+ [IMPROVEMENTS]
+ - better dependencies lists and META.yml (Alexandr Ciornii)
+ - ignore errors when loading configuration
+ - more accessors added to Test::Database::Handle
+
+1.07 Mon Oct 12 23:25:26 CEST 2009
+ [IMPROVEMENTS]
+ - Test::Database::Driver now has a dbd_version() method
+ - Fixed a bug that created some warnings (thanks to Nicholas Bamber)
+ - Test::Database::Driver::Pg now accepts a 'template' parameter
+ (requested by Adam Kennedy)
+ [TESTS]
+ - Fixed warnings in t/10-drivers.t and t/25-sql.t
+
+1.06 Thu Sep 3 00:39:49 CEST 2009
+ [IMPROVEMENTS]
+ - better basename computation for database created by the module
+ [DOCUMENTATION]
+ - added explanations on how database handles are provided
+ in Test::Database::Tutorial
+ [PREREQUISITES]
+ - Need YAML::Tiny 1.27, since we use LoadFile in scalar context
+
+1.05 Fri Aug 28 00:09:31 CEST 2009
+ [IMPROVEMENTS]
+ - the key configuration item allows to add a unique key
+ to database created by Test::Database (useful when
+ sharing a database between several test hosts)
+ [DOCUMENTATION]
+ - Test::Database::Tutorial now documents how to use the
+ module as a CPAN author or CPAN tester
+
+1.04 Sun Aug 23 03:10:11 CEST 2009
+ [IMPROVEMENTS]
+ - re-introduced Test::Database::Driver::Pg
+ - more robust computation of base_dir()
+ [TESTS]
+ - tests for make_dsn()
+
+1.03 Fri Aug 21 23:01:10 CEST 2009
+ [IMPROVEMENTS]
+ - improved dsn and driver_dsn management
+ - database requests may include version information
+ - more robust test suite
+
+1.02 Sun Aug 16 14:47:04 CEST 2009
+ [IMPROVEMENTS]
+ - re-introduced Test::Database::Driver::mysql
+ - add support for driver_dsn in configuration file
+
+1.01 Sun Aug 2 01:03:22 CEST 2009
+ [IMPROVEMENTS]
+ - re-introduced Test::Database::Driver
+ - Test::Database::Driver supports file-based DBD
+ - Test::Database::Driver maps existing databases to cwd()
+ - Drivers for SQLite, SQLite2, CSV, DBM
+
+1.00 Sat Jul 11 00:39:04 CEST 2009
+ [IMPROVEMENTS]
+ - rewrite/cleanup: the module now only supports a list of DSN
+ provided in the ~/.test-database configuration file
+ - the only two modules lefts for now are Test::Database and
+ Test::Database::Handle
+ [TODO]
+ - future versions will appear shortly and bring back some
+ of the features that appeared in 0.99 and later
+
+0.99_03 Tue Apr 6 22:16:05 CEST 2009
+ [DRIVERS]
+ - new driver for DBD::Pg
+
+0.99_02 Mon Apr 6 03:21:51 CEST 2009
+ [IMPROVEMENTS]
+ - try to connect to non file-based databases to ensure we can,
+ before adding a driver to our collection
+ - cleanup() will only clean loaded drivers
+
+0.99_01 Wed Apr 1 10:01:57 CEST 2009
+ [FIXES]
+ - Do not die when automatically trying to load a non-existent
+ ~/.test-database file
+ [DOCUMENTATION]
+ - add some documentation about REQUESTS
+ [TESTS]
+ - add tests for save_driver() and load_drivers()
+ - increase test coverage to over 95%
+
+0.99 Mon Mar 30 16:20:23 CEST 2009 - Perl QA Hackathon 2009
+ [FEATURES]
+ - completely redesigned interface: the module never starts a
+ database engine, but simply makes pre-configured ones available
+ to test scripts
+ [DRIVERS]
+ - new driver for DBD::SQlite
+ - new driver for DBD::SQlite2
+ - new driver for DBD::CSV
+ - new driver for DBD::DBM
+ - new driver for DBD::mysql
+
+0.02 Tue Oct 14 03:04:27 CEST 2008
+ [FEATURES]
+ - improved database engine setup process, using setup_engine(),
+ start_engine() and stop_engine() methods in the driver classes
+ [DRIVERS]
+ - add a driver for DBD::mysql
+ [TESTS]
+ - fix t/10-drivers.t to not fail on uninstalled DBD drivers
+
+0.01 Fri Oct 10 17:44:24 CEST 2008
+ [FEATURES]
+ - provide a simple interface for obtaining a database handle
+ [DRIVERS]
+ - add a driver for DBD::SQlite
+ - add a driver for DBD::CSV
+ - add a driver for DBD::DBM
+ [TESTS]
+ - over 97% test coverage
+
diff --git a/MANIFEST b/MANIFEST
new file mode 100644
index 0000000..7a81dfe
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,36 @@
+Build.PL
+Changes
+eg/MyDriver.pm
+lib/Test/Database.pm
+lib/Test/Database/Driver.pm
+lib/Test/Database/Driver/CSV.pm
+lib/Test/Database/Driver/DBM.pm
+lib/Test/Database/Driver/mysql.pm
+lib/Test/Database/Driver/Pg.pm
+lib/Test/Database/Driver/SQLite.pm
+lib/Test/Database/Driver/SQLite2.pm
+lib/Test/Database/Handle.pm
+lib/Test/Database/Tutorial.pod
+lib/Test/Database/Util.pm
+Makefile.PL
+MANIFEST This list of files
+META.yml
+README
+t/00-load.t
+t/08-handle.t
+t/09-handle-dsn.t
+t/10-drivers.t
+t/10-list_drivers.t
+t/11-available_dbname.t
+t/11-make_dsn.t
+t/11-version_matches.t
+t/12-load.t
+t/20-handles.t
+t/25-sql.t
+t/database.bad
+t/database.bad2
+t/database.empty
+t/database.good
+t/database.rc
+t/pod-coverage.t
+t/pod.t
diff --git a/META.yml b/META.yml
new file mode 100644
index 0000000..4dcb4e0
--- /dev/null
+++ b/META.yml
@@ -0,0 +1,46 @@
+---
+name: Test-Database
+version: 1.11
+author:
+ - 'Philippe Bruhat (BooK) <book@cpan.org>'
+abstract: Database handles ready for testing
+license: perl
+resources:
+ license: http://dev.perl.org/licenses/
+ repository: http://github.com/book/Test-Database
+requires:
+ DBI: 1
+ File::HomeDir: 0.5
+ File::Path: 0
+ File::Spec: 0
+ YAML::Tiny: 1.27
+ perl: 5.006
+ version: 0
+build_requires:
+ Test::More: 0
+provides:
+ Test::Database:
+ file: lib/Test/Database.pm
+ version: 1.11
+ Test::Database::Driver:
+ file: lib/Test/Database/Driver.pm
+ Test::Database::Driver::CSV:
+ file: lib/Test/Database/Driver/CSV.pm
+ Test::Database::Driver::DBM:
+ file: lib/Test/Database/Driver/DBM.pm
+ Test::Database::Driver::Pg:
+ file: lib/Test/Database/Driver/Pg.pm
+ Test::Database::Driver::SQLite:
+ file: lib/Test/Database/Driver/SQLite.pm
+ Test::Database::Driver::SQLite2:
+ file: lib/Test/Database/Driver/SQLite2.pm
+ Test::Database::Driver::mysql:
+ file: lib/Test/Database/Driver/mysql.pm
+ Test::Database::Handle:
+ file: lib/Test/Database/Handle.pm
+ Test::Database::Util:
+ file: lib/Test/Database/Util.pm
+generated_by: Module::Build version 0.33
+meta-spec:
+ url: http://module-build.sourceforge.net/META-spec-v1.4.html
+ version: 1.4
diff --git a/Makefile.PL b/Makefile.PL
new file mode 100644
index 0000000..3600f91
--- /dev/null
+++ b/Makefile.PL
@@ -0,0 +1,20 @@
+use strict;
+use warnings;
+use ExtUtils::MakeMaker;
+
+WriteMakefile(
+ NAME => 'Test::Database',
+ AUTHOR => 'Philippe Bruhat (BooK) <book@cpan.org>',
+ VERSION_FROM => 'lib/Test/Database.pm',
+ ABSTRACT_FROM => 'lib/Test/Database.pm',
+ PL_FILES => {},
+ PREREQ_PM => {
+ 'Test::More' => 0,
+ 'DBI' => 1,
+ 'File::HomeDir' => 0.50,
+ 'version' => 0,
+ 'YAML::Tiny' => 1.27,
+ },
+ dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', },
+ clean => { FILES => 'Test-Database-*' },
+);
diff --git a/README b/README
new file mode 100644
index 0000000..45300f6
--- /dev/null
+++ b/README
@@ -0,0 +1,58 @@
+Test-Database
+
+There's plenty of modules which need a database, and they all have
+to be configured differently and they're always a PITA when you first
+install and each and every time they upgrade. -- Michael Schwern
+
+Test::Database provides a simple way for test authors to request
+a test database, without worrying about environment variables or the
+test host configuration.
+
+
+INSTALLATION
+
+To install this module, run the following commands:
+
+ perl Makefile.PL
+ make
+ make test
+ make install
+
+Alternatively, to install with Module::Build, you can use the following commands:
+
+ perl Build.PL
+ ./Build
+ ./Build test
+ ./Build install
+
+SUPPORT AND DOCUMENTATION
+
+After installing, you can find documentation for this module with the
+perldoc command.
+
+ perldoc Test::Database
+
+You can also look for information at:
+
+ RT, CPAN's request tracker
+ http://rt.cpan.org/NoAuth/Bugs.html?Dist=Test-Database
+
+ AnnoCPAN, Annotated CPAN documentation
+ http://annocpan.org/dist/Test-Database
+
+ CPAN Ratings
+ http://cpanratings.perl.org/d/Test-Database
+
+ Search CPAN
+ http://search.cpan.org/dist/Test-Database
+
+
+COPYRIGHT
+
+Copyright (C) 2008-2009 Philippe Bruhat (BooK)
+
+LICENCE
+
+This module is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
diff --git a/eg/MyDriver.pm b/eg/MyDriver.pm
new file mode 100644
index 0000000..7e6f6e8
--- /dev/null
+++ b/eg/MyDriver.pm
@@ -0,0 +1,84 @@
+package Test::Database::Driver::MyDriver;
+use strict;
+use warnings;
+
+use Test::Database::Driver;
+our @ISA = qw( Test::Database::Driver );
+
+# uncomment only if your database engine is file-based
+#sub is_filebased {1}
+
+sub _version {
+ # return a version string
+}
+
+sub dsn {
+ my ($self, $dbname) = @_;
+ # return a dsn for $dbname
+}
+
+# this routine has a default implementation for file-based database engines
+sub create_database {
+ my ( $self, $dbname, $keep ) = @_;
+ $dbname = $self->available_dbname() if !$dbname;
+
+ # create the database if it doesn't exist
+ # ...
+
+ # return the handle
+ return Test::Database::Handle->new(
+ dsn => $self->dsn($dbname),
+ name => $dbname,
+ driver => $self,
+ # ... other fields, like username, password
+ );
+}
+
+sub drop_database {
+ my ( $self, $dbname ) = @_;
+
+ # drop the database
+}
+
+# this routine has a default implementation for file-based database engines
+sub databases {
+ my ($self) = @_;
+ # return the names of all databases existing in this driver
+}
+
+'MyDriver';
+
+__END__
+
+=head1 NAME
+
+Test::Database::Driver::MyDriver - A Test::Database driver for MyDriver
+
+=head1 SYNOPSIS
+
+ use Test::Database;
+ my @handles = Test::Database->handles( 'MyDriver' );
+
+=head1 DESCRIPTION
+
+This module is the C<Test::Database> driver for C<DBD::MyDriver>.
+
+=head1 SEE ALSO
+
+L<Test::Database::Driver>
+
+=head1 AUTHOR
+
+Philippe Bruhat (BooK), C<< <book@cpan.org> >>
+
+=head1 COPYRIGHT
+
+Copyright 2008-2009 Philippe Bruhat (BooK), all rights reserved.
+
+=head1 LICENSE
+
+This module is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+=cut
+
diff --git a/lib/Test/Database.pm b/lib/Test/Database.pm
new file mode 100644
index 0000000..5f0d7a3
--- /dev/null
+++ b/lib/Test/Database.pm
@@ -0,0 +1,475 @@
+package Test::Database;
+use 5.006;
+use warnings;
+use strict;
+
+use File::HomeDir;
+use File::Spec;
+use DBI;
+use Carp;
+
+use Test::Database::Util;
+use Test::Database::Driver;
+use Test::Database::Handle;
+
+our $VERSION = '1.11';
+
+#
+# global configuration
+#
+
+# internal data structures
+my @HANDLES;
+my @DRIVERS;
+
+# driver information
+my @DRIVERS_OUR;
+my @DRIVERS_OK;
+
+# find the list of all drivers we support
+sub load_drivers {
+ my %seen;
+ for my $dir (@INC) {
+ opendir my $dh, File::Spec->catdir( $dir, qw( Test Database Driver ) )
+ or next;
+ $seen{$_}++ for map { s/\.pm$//; $_ } grep {/\.pm$/} readdir $dh;
+ closedir $dh;
+ }
+
+ # drivers we support
+ @DRIVERS_OUR = sort keys %seen;
+
+ # available DBI drivers
+ my %DRIVERS_DBI = map { $_ => 1 } DBI->available_drivers();
+
+ # supported
+ @DRIVERS_OK = grep { exists $DRIVERS_DBI{$_} } @DRIVERS_OUR;
+
+ # automatically load all drivers in @DRIVERS_OK
+ # (but ignore compilation errors)
+ eval "require Test::Database::Driver::$_" for @DRIVERS_OK;
+
+ # actual driver objects
+ @DRIVERS = map {
+ my $driver;
+ eval { $driver = Test::Database::Driver->new( dbd => $_ ); 1; }
+ or warn "$@\n";
+ $driver || ();
+ }
+ grep { "Test::Database::Driver::$_"->is_filebased() } @DRIVERS_OK;
+}
+
+# startup configuration
+__PACKAGE__->load_drivers();
+__PACKAGE__->load_config() if -e _rcfile();
+
+#
+# private functions
+#
+# location of our resource file
+sub _rcfile {
+ File::Spec->catfile( File::HomeDir->my_data(), '.test-database' );
+}
+
+#
+# methods
+#
+sub clean_config {
+ @HANDLES = ();
+ @DRIVERS = ();
+}
+
+sub load_config {
+ my ( $class, @files ) = @_;
+ @files = ( _rcfile() ) if !@files;
+
+ # fetch the items (dsn, driver_dsn) from the config files
+ my @items = map { _read_file($_) } @files;
+
+ # load the key
+ Test::Database::Driver->_set_key( $_->{key} )
+ for grep { exists $_->{key} } @items;
+
+ # create the handles
+ push @HANDLES,
+ map { eval { Test::Database::Handle->new(%$_) } || () }
+ grep { exists $_->{dsn} } @items;
+
+ # create the drivers
+ push @DRIVERS,
+ map { eval { Test::Database::Driver->new(%$_) } || () }
+ grep { exists $_->{driver_dsn} } @items;
+}
+
+sub list_drivers {
+ my ( $class, $type ) = @_;
+ $type ||= '';
+ return
+ $type eq 'all' ? @DRIVERS_OUR
+ : $type eq 'available' ? @DRIVERS_OK
+ : map { $_->name() } @DRIVERS;
+}
+
+sub drivers { @DRIVERS }
+
+# requests for handles
+sub handles {
+ my ( $class, @requests ) = @_;
+ my @handles;
+
+ # empty request means "everything"
+ return @handles = ( @HANDLES, map { $_->make_handle() } @DRIVERS )
+ if !@requests;
+
+ # turn strings (driver name) into actual requests
+ @requests = map { (ref) ? $_ : { dbd => $_ } } @requests;
+
+ # process parameter aliases
+ $_->{dbd} ||= delete $_->{driver} for @requests;
+
+ # get the matching handles
+ for my $handle (@HANDLES) {
+ my $ok;
+ my $driver = $handle->{driver};
+ for my $request (@requests) {
+ next if $request->{dbd} ne $handle->dbd();
+ if ( grep /version/, keys %$request ) {
+ next if !$driver || !$driver->version_matches($request);
+ }
+ $ok = 1;
+ last;
+ }
+ push @handles, $handle if $ok;
+ }
+
+ # get the matching drivers
+ my @drivers;
+ for my $driver (@DRIVERS) {
+ my $ok;
+ for my $request (@requests) {
+ next if $request->{dbd} ne $driver->dbd();
+ next if !$driver->version_matches($request);
+ $ok = 1;
+ last;
+ }
+ push @drivers, $driver if $ok;
+ }
+
+ # get a new database handle from the drivers
+ push @handles, map { $_->make_handle() } @drivers;
+
+ # then on the handles
+ return @handles;
+}
+
+sub handle {
+ my @h = shift->handles(@_);
+ return @h ? $h[0] : ();
+}
+
+'TRUE';
+
+__END__
+
+=head1 NAME
+
+Test::Database - Database handles ready for testing
+
+=head1 SYNOPSIS
+
+Maybe you wrote generic code you want to test on all available databases:
+
+ use Test::More;
+ use Test::Database;
+
+ # get all available handles
+ my @handles = Test::Database->handles();
+
+ # plan the tests
+ plan tests => 3 + 4 * @handles;
+
+ # run the tests
+ for my $handle (@handles) {
+ diag "Testing with " . $handle->dbd(); # mysql, SQLite, etc.
+
+ # there are several ways to access the dbh:
+
+ # let $handle do the connect()
+ my $dbh = $handle->dbh();
+
+ # do the connect() yourself
+ my $dbh = DBI->connect( $handle->connection_info() );
+ my $dbh = DBI->connect( $handle->dsn(), $handle->username(),
+ $handle->password() );
+ }
+
+It's possible to limit the results, based on the databases your code
+supports:
+
+ my @handles = Test::Database->handles(
+ 'SQLite', # SQLite database
+ { dbd => 'mysql' }, # or mysql database
+ { driver => 'Pg' }, # or Postgres database
+ );
+
+ # use them as above
+
+If you only need a single database handle, all the following return
+the same one:
+
+ my $handle = ( Test::Database->handles(@requests) )[0];
+ my ($handle) = Test::Database->handles(@requests);
+ my $handle = Test::Database->handles(@requests); # scalar context
+ my $handle = Test::Database->handle(@requests); # singular!
+ my @handles = Test::Database->handle(@requests); # one or zero item
+
+You can use the same requests again if you need to use the same
+test databases over several test scripts.
+
+=head1 DESCRIPTION
+
+Quoting Michael Schwern:
+
+I<There's plenty of modules which need a database, and they all have
+to be configured differently and they're always a PITA when you first
+install and each and every time they upgrade.>
+
+I<User setup can be dealt with by making Test::Database a build
+dependency. As part of Test::Database's install process it walks the
+user through the configuration process. Once it's done, it writes out
+a config file and then it's done for good.>
+
+See L<http://www.nntp.perl.org/group/perl.qa/2008/10/msg11645.html>
+for the thread that led to the creation of C<Test::Database>.
+
+C<Test::Database> provides a simple way for test authors to request
+a test database, without worrying about environment variables or the
+test host configuration.
+
+See L<SYNOPSIS> for typical usage.
+
+=head1 METHODS
+
+C<Test::Database> provides the following methods:
+
+=over 4
+
+=item list_drivers( [$type] )
+
+Return a list of driver names of the given "type".
+
+C<all> returns the list of all existing C<Test::Database::Driver> subclasses.
+
+C<available> returns the list of C<Test::Database::Driver> subclasses for which the matching
+C<DBD> class is available.
+
+Called with no parameter (or anything not matching C<all> or C<available>), it will return
+the list of currently loaded drivers.
+
+=item drivers()
+
+Returns the C<Test::Database::Driver> instances that are setup by
+C<load_drivers()> and updated by C<load_config()>.
+
+=item load_drivers()
+
+Load the available drivers from the system (file-based drivers, usually).
+
+=item load_config( @files )
+
+Read configuration from the files in C<@files>.
+
+If no file is provided, the local equivalent of F<~/.test-database> is used.
+
+=item clean_config()
+
+Empties whatever configuration has already been loaded.
+Also removes the loaded drivers list.
+
+=item handles( @requests )
+
+Return a set of C<Test::Database::Handle> objects that match the
+given C<@requests>.
+
+If C<@requests> is not provided, return all the available handles.
+
+See L<REQUESTS> for details about writing requests.
+
+=item handle( @request )
+
+I<Singular> version of C<handles()>, that returns the first matching
+handle.
+
+=back
+
+=head1 REQUESTS
+
+The C<handles()> method takes I<requests> as parameters. A request is
+a simple hash reference, with a number of recognized keys.
+
+=over 4
+
+=item *
+
+C<dbd>: driver name (based on the C<DBD::> name).
+
+C<driver> is an alias for C<dbd>.
+If the two keys are present, the C<driver> key will be ignored.
+
+If missing, all available drivers will match.
+
+=item *
+
+C<version>: exact database engine version
+
+Only database engines having a version string identical to the
+given version string will match.
+
+=item *
+
+C<min_version>: minimum database engine version
+
+Only database engines having a version number greater or equal to the
+given minimum version will match.
+
+=item *
+
+C<max_version>: maximum database engine version
+
+Only database engines having a version number lower (and not equal) to the
+given maximum version will match.
+
+=item *
+
+C<regex_version>: matching database engine version
+
+Only database engines having a version string that matches the
+given regular expression will match.
+
+=back
+
+A request can also consist of a single string, in which case it is
+interpreted as a shortcut for C<{ dbd => $string }>.
+
+=head1 FILES
+
+The list of available, authorized DSN is stored in the local equivalent
+of F<~/.test-database>. It's a simple list of key/value pairs, with the
+C<dsn>, C<driver_dsn> or C<key> keys being used to split successive entries:
+
+ # mysql
+ dsn = dbi:mysql:database=mydb;host=localhost;port=1234
+ username = user
+ password = s3k r3t
+
+ # Oracle
+ dsn = dbi:Oracle:test
+
+ # set a unique key when creating databases
+ key = thwapp
+
+ # a "driver" with full access (create/drop databases)
+ driver_dsn = dbi:mysql:
+ username = root
+
+The C<username> and C<password> keys are optional and empty strings will be
+used if they are not provided.
+
+Empty lines and comments are ignored.
+
+Optionaly, the C<key> section is used to add a "unique" element to the
+databases created by the drivers (as defined by C<driver_dsn>). It
+allows several hosts to share access to the same database server
+without risking a race condition when creating a new database. See
+L<Test::Database::Tutorial> for a longer explanation.
+
+Individual drivers may accept extra parameters. See their documetation
+for details. Unrecognized parameters and not used, and therefore ignored.
+
+=head1 AUTHOR
+
+Philippe Bruhat (BooK), C<< <book@cpan.org> >>
+
+=head1 BUGS
+
+Please report any bugs or feature requests to C<bug-test-database at rt.cpan.org>, or through
+the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Test-Database>. I will be notified, and then you'll
+automatically be notified of progress on your bug as I make changes.
+
+=head1 SUPPORT
+
+You can find documentation for this module with the perldoc command.
+
+ perldoc Test::Database
+
+You can also look for information at:
+
+=over 4
+
+=item * RT: CPAN's request tracker
+
+L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Test-Database>
+
+=item * AnnoCPAN: Annotated CPAN documentation
+
+L<http://annocpan.org/dist/Test-Database>
+
+=item * CPAN Ratings
+
+L<http://cpanratings.perl.org/d/Test-Database>
+
+=item * Search CPAN
+
+L<http://search.cpan.org/dist/Test-Database>
+
+=back
+
+=head1 TODO
+
+Some of the items on the TODO list:
+
+=over 4
+
+=item *
+
+Add a database engine autodetection script/module, to automatically
+write the F<.test-database> configuration file.
+
+=back
+
+=head1 ACKNOWLEDGEMENTS
+
+Thanks to C<< <perl-qa@perl.org> >> for early comments.
+
+Thanks to Nelson Ferraz for writing C<DBIx::Slice>, the testing of
+which made me want to have a generic way to obtain a test database.
+
+Thanks to Mark Lawrence for discussing this module with me, and
+sending me an alternative implementation to show me what he needed.
+
+Thanks to Kristian Koehntopp for helping me write a mysql driver,
+and to Greg Sabino Mullane for writing a full Postgres driver,
+none of which made it into the final release because of the complete
+change in goals and implementation between versions 0.02 and 0.03.
+
+The work leading to the new implementation (version 0.99 and later)
+was carried on during the Perl QA Hackathon, held in Birmingham in March
+2009. Thanks to Birmingham.pm for organizing it and to Booking.com for
+sending me there.
+
+Thanks to the early adopters:
+Alexis Sukrieh (SUKRIA),
+Nicholas Bamber (SILASMONK)
+and Adam Kennedy (ADAMK).
+
+=head1 COPYRIGHT
+
+Copyright 2008-2010 Philippe Bruhat (BooK), all rights reserved.
+
+=head1 LICENSE
+
+This module is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+=cut
+
diff --git a/lib/Test/Database/Driver.pm b/lib/Test/Database/Driver.pm
new file mode 100644
index 0000000..4a4a3a5
--- /dev/null
+++ b/lib/Test/Database/Driver.pm
@@ -0,0 +1,527 @@
+package Test::Database::Driver;
+use strict;
+use warnings;
+use Carp;
+use File::Spec;
+use File::Path;
+use version;
+use YAML::Tiny qw( LoadFile DumpFile );
+use Cwd;
+
+use Test::Database::Handle;
+
+#
+# GLOBAL CONFIGURATION
+#
+
+# the location where all drivers-related files will be stored
+my $KEY = '';
+my $login = getlogin() || getpwuid($<);
+$login =~ s/\W+//g;
+my $root = File::Spec->rel2abs(
+ File::Spec->catdir( File::Spec->tmpdir(), "Test-Database-$login" ) );
+
+# generic driver class initialisation
+sub __init {
+ my ($class) = @_;
+
+ # create directory if needed
+ my $dir = $class->base_dir();
+ if ( !-e $dir ) {
+ mkpath( [$dir] );
+ }
+ elsif ( !-d $dir ) {
+ croak "$dir is not a directory. Initializing $class failed";
+ }
+
+ # load the DBI driver (may die)
+ DBI->install_driver( $class->name() );
+}
+
+#
+# METHODS
+#
+sub new {
+ my ( $class, %args ) = @_;
+
+ if ( $class eq __PACKAGE__ ) {
+ if ( exists $args{driver_dsn} ) {
+ my ( $scheme, $driver, $attr_string, $attr_hash, $driver_dsn )
+ = DBI->parse_dsn( $args{driver_dsn} );
+ $args{dbd} = $driver;
+ }
+ croak "dbd or driver_dsn parameter required" if !exists $args{dbd};
+ eval "require Test::Database::Driver::$args{dbd}"
+ or do { $@ =~ s/ at .*?\z//s; croak $@; };
+ $class = "Test::Database::Driver::$args{dbd}";
+ $class->__init();
+ }
+
+ my $self = bless {
+ username => '',
+ password => '',
+ %args,
+ dbd => $class->name() || $args{dbd},
+ },
+ $class;
+
+ $self->_load_mapping();
+
+ # try to connect before returning the object
+ if ( !$class->is_filebased() ) {
+ eval {
+ DBI->connect_cached( $self->connection_info(),
+ { PrintError => 0 } );
+ } or return;
+ }
+
+ return $self;
+}
+
+sub _mapping_file {
+ return File::Spec->catfile( $_[0]->base_dir(), 'mapping.yml' );
+}
+
+sub available_dbname {
+ my ($self) = @_;
+ my $name = $self->_basename();
+ my %taken = map { $_ => 1 } $self->databases();
+ my $n = 0;
+ $n++ while $taken{"$name$n"};
+ return "$name$n";
+}
+
+sub _load_mapping {
+ my ($self, $file)= @_;
+ $file = $self->_mapping_file() if ! defined $file;
+
+ # basic mapping info
+ $self->{mapping} = {};
+ return if !-e $file;
+
+ # load mapping from file
+ my $mapping = LoadFile( $file );
+ $self->{mapping} = $mapping->{$self->driver_dsn()} || {};
+
+ # remove stale entries
+ $self->_save_mapping( $file ) if $self->_check_mapping();
+}
+
+sub _save_mapping {
+ my ($self, $file )= @_;
+ $file = $self->_mapping_file() if ! defined $file;
+
+ # update mapping information
+ my $mapping = {};
+ $mapping = LoadFile( $file ) if -e $file;
+ $mapping->{ $self->driver_dsn() } = $self->{mapping};
+
+ # save mapping information
+ DumpFile( "$file.tmp", $mapping );
+ rename "$file.tmp", $file
+ or croak "Can't rename $file.tmp to $file: $!";
+}
+
+sub _check_mapping {
+ my ($self) = @_;
+ my $mapping = $self->{mapping};
+ my %database = map { $_ => undef } $self->databases();
+ my $updated;
+
+ # check that all databases in the mapping exist
+ for my $cwd ( keys %$mapping ) {
+ if ( !exists $database{ $mapping->{$cwd} } ) {
+ delete $mapping->{$cwd};
+ $updated++;
+ }
+ }
+ return $updated;
+}
+
+sub make_dsn {
+ my ($self, @args, @pairs) = @_;
+
+ push @pairs, join '=', splice @args, 0, 2 while @args;
+
+ my $dsn = $self->driver_dsn();
+ return $dsn
+ . ( $dsn =~ /^dbi:[^:]+:$/ ? '' : ';' )
+ . join( ';', @pairs );
+}
+
+sub make_handle {
+ my ($self) = @_;
+ my $handle;
+
+ # get the database name from the mapping
+ my $dbname = $self->{mapping}{ cwd() };
+
+ # if the database still exists, return it
+ if ( $dbname && grep { $_ eq $dbname } $self->databases() ) {
+ $handle = Test::Database::Handle->new(
+ dsn => $self->dsn($dbname),
+ username => $self->username(),
+ password => $self->password(),
+ name => $dbname,
+ driver => $self,
+ );
+ }
+
+ # otherwise create the database and update the mapper
+ else {
+ $handle = $self->create_database();
+ $self->{mapping}{ cwd() } = $handle->{name};
+ $self->_save_mapping();
+ }
+
+ return $handle;
+}
+
+sub version_matches {
+ my ( $self, $request ) = @_;
+
+ # string tests
+ my $version_string = $self->version_string();
+ return
+ if exists $request->{version}
+ && $version_string ne $request->{version};
+ return
+ if exists $request->{regex_version}
+ && $version_string !~ $request->{regex_version};
+
+ # numeric tests
+ my $version = $self->version();
+ return
+ if exists $request->{min_version}
+ && $version < $request->{min_version};
+ return
+ if exists $request->{max_version}
+ && $version >= $request->{max_version};
+
+ return 1;
+}
+
+#
+# ACCESSORS
+#
+sub name { return ( $_[0] =~ /^Test::Database::Driver::([:\w]*)/g )[0]; }
+*dbd = \&name;
+
+sub base_dir {
+ my ($self) = @_;
+ my $class = ref $self || $self;
+ return $root if $class eq __PACKAGE__;
+ my $dir = File::Spec->catdir( $root, $class->name() );
+ return $dir if !ref $self; # class method
+ return $self->{base_dir} ||= $dir; # may be overriden in new()
+}
+
+sub version {
+ no warnings;
+ return $_[0]{version}
+ ||= version->new( $_[0]->_version() =~ /^([0-9._]*[0-9])/ );
+}
+
+sub version_string {
+ return $_[0]{version_string} ||= $_[0]->_version();
+}
+
+sub dbd_version { return "DBD::$_[0]{dbd}"->VERSION; }
+
+sub driver_dsn { return $_[0]{driver_dsn} ||= $_[0]->_driver_dsn() }
+sub username { return $_[0]{username} }
+sub password { return $_[0]{password} }
+
+sub connection_info {
+ return ( $_[0]->driver_dsn(), $_[0]->username(), $_[0]->password() );
+}
+
+# THESE MUST BE IMPLEMENTED IN THE DERIVED CLASSES
+sub drop_database { die "$_[0] doesn't have a drop_database() method\n" }
+sub _version { die "$_[0] doesn't have a _version() method\n" }
+
+# create_database creates the database and returns a handle
+sub create_database {
+ my $class = ref $_[0] || $_[0];
+ goto &_filebased_create_database if $class->is_filebased();
+ die "$class doesn't have a create_database() method\n";
+}
+
+sub databases {
+ goto &_filebased_databases if $_[0]->is_filebased();
+ die "$_[0] doesn't have a databases() method\n";
+}
+
+# THESE MAY BE OVERRIDDEN IN THE DERIVED CLASSES
+sub is_filebased {0}
+sub _driver_dsn { join ':', 'dbi', $_[0]->name(), ''; }
+
+sub dsn {
+ my ( $self, $dbname ) = @_;
+ return $self->make_dsn( database => $dbname );
+}
+
+#
+# PRIVATE METHODS
+#
+sub _set_key {
+ $KEY = $_[1] || '';
+ croak "Invalid format for key '$KEY'" if $KEY !~ /^\w*$/;
+}
+
+sub _basename {
+ lc join '_', 'TDD', $_[0]->name(), $login, ( $KEY ? $KEY : (), '' );
+}
+
+# generic implementations for file-based drivers
+sub _filebased_databases {
+ my ($self) = @_;
+ my $dir = $self->base_dir();
+ my $basename = qr/^@{[$self->_basename()]}/;
+
+ opendir my $dh, $dir or croak "Can't open directory $dir for reading: $!";
+ my @databases = grep {/$basename/} File::Spec->no_upwards( readdir($dh) );
+ closedir $dh;
+
+ return @databases;
+}
+
+sub _filebased_create_database {
+ my ( $self ) = @_;
+ my $dbname = $self->available_dbname();
+
+ return Test::Database::Handle->new(
+ dsn => $self->dsn($dbname),
+ name => $dbname,
+ driver => $self,
+ );
+}
+
+'CONNECTION';
+
+__END__
+
+=head1 NAME
+
+Test::Database::Driver - Base class for Test::Database drivers
+
+=head1 SYNOPSIS
+
+ package Test::Database::Driver::MyDatabase;
+ use strict;
+ use warnings;
+
+ use Test::Database::Driver;
+ our @ISA = qw( Test::Database::Driver );
+
+ sub _version {
+ my ($class) = @_;
+ ...;
+ return $version;
+ }
+
+ sub create_database {
+ my ( $self ) = @_;
+ ...;
+ return $handle;
+ }
+
+ sub drop_database {
+ my ( $self, $name ) = @_;
+ ...;
+ }
+
+ sub databases {
+ my ($self) = @_;
+ ...;
+ return @databases;
+ }
+
+=head1 DESCRIPTION
+
+C<Test::Database::Driver> is a base class for creating C<Test::Database>
+drivers.
+
+=head1 METHODS
+
+The class provides the following methods:
+
+=over 4
+
+=item new( %args )
+
+Create a new C<Test::Database::Driver> object.
+
+If called as C<< Test::Database::Driver->new() >>, requires a C<driver>
+parameter to define the actual object class.
+
+=item make_handle()
+
+Create a new C<Test::Database::Handle> object, attached to an existing database
+or to a newly created one.
+
+The decision whether to create a new database or not is made by
+C<Test::Database::Driver> based on the information in the mapper.
+See L<TEMPORARY STORAGE ORGANIZATION> for details.
+
+=item make_dsn( %args )
+
+Return a Data Source Name based on the driver's DSN, with the key/value
+pairs contained in C<%args> as additional parameters.
+
+This is typically used by C<dsn()> to make a DSN for a specific database,
+based on the driver's DSN.
+
+=item name()
+
+=item dbd()
+
+The driver's short name (everything after C<Test::Database::Driver::>).
+
+=item base_dir()
+
+The directory where the driver should store all the files for its databases,
+if needed. Typically used by file-based database drivers.
+
+=item version()
+
+C<version> object representing the version of the underlying database enginge.
+This object is build with the return value of C<_version()>.
+
+=item version_string()
+
+Version string representing the version of the underlying database enginge.
+This string is the actual return value of C<_version()>.
+
+=item dbd_version()
+
+The version of the DBD used to connect to the database engine, as returned
+by C<VERSION()>.
+
+=item driver_dsn()
+
+Return a driver Data Source Name, sufficient to connect to the database
+engine without specifying an actual database.
+
+=item username()
+
+Return the connection username.
+
+=item password()
+
+Return the connection password.
+
+=item connection_info()
+
+Return the connection information triplet (C<driver_dsn>, C<username>,
+C<password>).
+
+=item version_matches( $request )
+
+Return a boolean indicating if the driver's version matches the version
+constraints in the given request (see L<Test::Database> documentation's
+section about requests).
+
+=back
+
+The class also provides a few helpful commands that may be useful for driver
+authors:
+
+=over 4
+
+=item available_dbname()
+
+Return an unused database name that can be used to create a new database
+for the driver.
+
+=item dsn( $dbname )
+
+Build a Data Source Name for the database with the given C<$dbname>,
+based on the driver's DSN.
+
+=back
+
+=head1 WRITING A DRIVER FOR YOUR DATABASE OF CHOICE
+
+The L<SYNOPSIS> contains a good template for writing a
+C<Test::Database::Driver> class.
+
+Creating a driver requires writing the following methods:
+
+=over 4
+
+=item _version()
+
+Return the version of the underlying database engine.
+
+=item create_database( $name )
+
+Create the database for the corresponding DBD driver.
+
+Return a C<Test::Database::Handle> in case of success, and nothing in
+case of failure to create the database.
+
+=item drop_database( $name )
+
+Drop the database named C<$name>.
+
+=back
+
+Some methods have defaults implementations in C<Test::Database::Driver>,
+but those can be overridden in the derived class:
+
+=over 4
+
+=item is_filebased()
+
+Return a boolean value indicating if the database engine is file-based
+or not, i.e. if all the database information is stored in a file or a
+directory, and no external database server is needed.
+
+=item databases()
+
+Return the names of all existing databases for this driver as a list
+(the default implementation is only valid for file-based drivers).
+
+=back
+
+=head1 TEMPORARY STORAGE ORGANIZATION
+
+Subclasses of C<Test::Database::Driver> store useful information
+in the system's temporary directory, under a directory named
+F<Test-Database-$user> (C<$user> being the current user's name).
+
+That directory contains the following files:
+
+=over 4
+
+=item database files
+
+The database files and directories created by file-based drivers
+controlled by C<Test::Database> are stored here, under names matching
+F<tdd_B<DRIVER>_B<N>>, where B<DRIVER> is the lowercased name of the
+driver and B<N> is a number.
+
+=item the F<mapping.yml> file
+
+A YAML file containing a C<cwd()> / database name mapping, to enable a
+given test suite to receive the same database handles in all the test
+scripts that call the C<Test::Database->handles()> method.
+
+=back
+
+=head1 AUTHOR
+
+Philippe Bruhat (BooK), C<< <book@cpan.org> >>
+
+=head1 COPYRIGHT
+
+Copyright 2008-2010 Philippe Bruhat (BooK), all rights reserved.
+
+=head1 LICENSE
+
+This module is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+=cut
+
diff --git a/lib/Test/Database/Driver/CSV.pm b/lib/Test/Database/Driver/CSV.pm
new file mode 100644
index 0000000..915f0e9
--- /dev/null
+++ b/lib/Test/Database/Driver/CSV.pm
@@ -0,0 +1,63 @@
+package Test::Database::Driver::CSV;
+use strict;
+use warnings;
+
+use File::Spec;
+use File::Path;
+
+use Test::Database::Driver;
+our @ISA = qw( Test::Database::Driver );
+
+sub is_filebased {1}
+
+sub _version { return Text::CSV_XS->VERSION; }
+
+sub dsn {
+ my ( $self, $dbname ) = @_;
+ my $dbdir = File::Spec->catdir( $self->base_dir(), $dbname );
+ mkpath( [$dbdir] );
+ return $self->make_dsn( f_dir => $dbdir );
+}
+
+sub drop_database {
+ my ( $self, $dbname ) = @_;
+ my $dbdir = File::Spec->catdir( $self->base_dir(), $dbname );
+ rmtree( [$dbdir] );
+}
+
+'CSV';
+
+__END__
+
+=head1 NAME
+
+Test::Database::Driver::CSV - A Test::Database driver for CSV
+
+=head1 SYNOPSIS
+
+ use Test::Database;
+ my @handles = Test::Database->handles( 'CSV' );
+
+=head1 DESCRIPTION
+
+This module is the C<Test::Database> driver for C<DBD::CSV>.
+
+=head1 SEE ALSO
+
+L<Test::Database::Driver>
+
+=head1 AUTHOR
+
+Philippe Bruhat (BooK), C<< <book@cpan.org> >>
+
+=head1 COPYRIGHT
+
+Copyright 2008-2010 Philippe Bruhat (BooK), all rights reserved.
+
+=head1 LICENSE
+
+This module is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+=cut
+
diff --git a/lib/Test/Database/Driver/DBM.pm b/lib/Test/Database/Driver/DBM.pm
new file mode 100644
index 0000000..e2751fe
--- /dev/null
+++ b/lib/Test/Database/Driver/DBM.pm
@@ -0,0 +1,64 @@
+package Test::Database::Driver::DBM;
+use strict;
+use warnings;
+
+use File::Spec;
+use File::Path;
+use DBD::DBM;
+
+use Test::Database::Driver;
+our @ISA = qw( Test::Database::Driver );
+
+sub is_filebased {1}
+
+sub _version { return DBD::DBM->VERSION; }
+
+sub dsn {
+ my ( $self, $dbname ) = @_;
+ my $dbdir = File::Spec->catdir( $self->base_dir(), $dbname );
+ mkpath( [$dbdir] );
+ return $self->make_dsn( f_dir => $dbdir );
+}
+
+sub drop_database {
+ my ( $self, $dbname ) = @_;
+ my $dbdir = File::Spec->catdir( $self->base_dir(), $dbname );
+ rmtree( [$dbdir] );
+}
+
+'DBM';
+
+__END__
+
+=head1 NAME
+
+Test::Database::Driver::DBM - A Test::Database driver for DBM
+
+=head1 SYNOPSIS
+
+ use Test::Database;
+ my @handles = Test::Database->handles( 'DBM' );
+
+=head1 DESCRIPTION
+
+This module is the C<Test::Database> driver for C<DBD::DBM>.
+
+=head1 SEE ALSO
+
+L<Test::Database::Driver>
+
+=head1 AUTHOR
+
+Philippe Bruhat (BooK), C<< <book@cpan.org> >>
+
+=head1 COPYRIGHT
+
+Copyright 2008-2010 Philippe Bruhat (BooK), all rights reserved.
+
+=head1 LICENSE
+
+This module is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+=cut
+
diff --git a/lib/Test/Database/Driver/Pg.pm b/lib/Test/Database/Driver/Pg.pm
new file mode 100644
index 0000000..106b909
--- /dev/null
+++ b/lib/Test/Database/Driver/Pg.pm
@@ -0,0 +1,100 @@
+package Test::Database::Driver::Pg;
+use strict;
+use warnings;
+use Carp;
+
+use Test::Database::Driver;
+our @ISA = qw( Test::Database::Driver );
+
+sub _version {
+ DBI->connect_cached( $_[0]->connection_info() )
+ ->selectcol_arrayref('SELECT VERSION()')->[0] =~ /^PostgreSQL (\S+)/;
+ return $1;
+}
+
+sub create_database {
+ my ($self) = @_;
+ my $dbname = $self->available_dbname();
+
+ DBI->connect_cached( $self->connection_info() )
+ ->do( "CREATE DATABASE $dbname"
+ . ( $self->{template} ? " TEMPLATE $self->{template}" : '' ) );
+
+ # return the handle
+ return Test::Database::Handle->new(
+ dsn => $self->dsn($dbname),
+ name => $dbname,
+ username => $self->username(),
+ password => $self->password(),
+ driver => $self,
+ );
+}
+
+sub drop_database {
+ my ( $self, $dbname ) = @_;
+
+ DBI->connect_cached( $self->connection_info() )
+ ->do("DROP DATABASE $dbname")
+ if grep { $_ eq $dbname } $self->databases();
+}
+
+sub databases {
+ my ($self) = @_;
+ my $basename = qr/^@{[$self->_basename()]}/;
+ my $databases = eval {
+ DBI->connect_cached( $self->connection_info(), { PrintError => 0 } )
+ ->selectall_arrayref(
+ 'SELECT datname FROM pg_catalog.pg_database');
+ };
+ return grep {/$basename/} map {@$_} @$databases;
+}
+
+'Pg';
+
+__END__
+
+=head1 NAME
+
+Test::Database::Driver::Pg - A Test::Database driver for Pg
+
+=head1 SYNOPSIS
+
+ use Test::Database;
+ my @handles = Test::Database->handles( 'Pg' );
+
+=head1 DESCRIPTION
+
+This module is the C<Test::Database> driver for C<DBD::Pg>.
+
+=head1 EXTRA PARAMETERS
+
+This driver understands the following extra parameters in the configuration
+file:
+
+=over 4
+
+=item template
+
+The template to use when creating a new database.
+
+=back
+
+=head1 SEE ALSO
+
+L<Test::Database::Driver>
+
+=head1 AUTHOR
+
+Philippe Bruhat (BooK), C<< <book@cpan.org> >>
+
+=head1 COPYRIGHT
+
+Copyright 2008-2010 Philippe Bruhat (BooK), all rights reserved.
+
+=head1 LICENSE
+
+This module is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+=cut
+
diff --git a/lib/Test/Database/Driver/SQLite.pm b/lib/Test/Database/Driver/SQLite.pm
new file mode 100644
index 0000000..0065cb9
--- /dev/null
+++ b/lib/Test/Database/Driver/SQLite.pm
@@ -0,0 +1,62 @@
+package Test::Database::Driver::SQLite;
+use strict;
+use warnings;
+
+use Test::Database::Driver;
+our @ISA = qw( Test::Database::Driver );
+
+use DBI;
+use File::Spec;
+
+sub is_filebased {1}
+
+sub _version { return DBI->connect( $_[0]->driver_dsn() )->{sqlite_version}; }
+
+sub dsn {
+ my ( $self, $dbname ) = @_;
+ return $self->make_dsn(
+ dbname => File::Spec->catdir( $self->base_dir(), $dbname ) );
+}
+
+sub drop_database {
+ my ( $self, $dbname ) = @_;
+ my $dbfile = File::Spec->catfile( $self->base_dir(), $dbname );
+ unlink $dbfile;
+}
+
+'SQLite';
+
+__END__
+
+=head1 NAME
+
+Test::Database::Driver::SQLite - A Test::Database driver for SQLite
+
+=head1 SYNOPSIS
+
+ use Test::Database;
+ my @handles = Test::Database->handles( 'SQLite' );
+
+=head1 DESCRIPTION
+
+This module is the C<Test::Database> driver for C<DBD::SQLite>.
+
+=head1 SEE ALSO
+
+L<Test::Database::Driver>
+
+=head1 AUTHOR
+
+Philippe Bruhat (BooK), C<< <book@cpan.org> >>
+
+=head1 COPYRIGHT
+
+Copyright 2008-2010 Philippe Bruhat (BooK), all rights reserved.
+
+=head1 LICENSE
+
+This module is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+=cut
+
diff --git a/lib/Test/Database/Driver/SQLite2.pm b/lib/Test/Database/Driver/SQLite2.pm
new file mode 100644
index 0000000..d5d5c92
--- /dev/null
+++ b/lib/Test/Database/Driver/SQLite2.pm
@@ -0,0 +1,62 @@
+package Test::Database::Driver::SQLite2;
+use strict;
+use warnings;
+
+use Test::Database::Driver;
+our @ISA = qw( Test::Database::Driver );
+
+use DBI;
+use File::Spec;
+
+sub is_filebased {1}
+
+sub _version { return DBI->connect( $_[0]->driver_dsn() )->{sqlite_version}; }
+
+sub dsn {
+ my ( $self, $dbname ) = @_;
+ return $self->make_dsn(
+ dbname => File::Spec->catdir( $self->base_dir(), $dbname ) );
+}
+
+sub drop_database {
+ my ( $self, $dbname ) = @_;
+ my $dbfile = File::Spec->catfile( $self->base_dir(), $dbname );
+ unlink $dbfile;
+}
+
+'SQLite2';
+
+__END__
+
+=head1 NAME
+
+Test::Database::Driver::SQLite2 - A Test::Database driver for SQLite2
+
+=head1 SYNOPSIS
+
+ use Test::Database;
+ my @handles = Test::Database->handles( 'SQLite2' );
+
+=head1 DESCRIPTION
+
+This module is the C<Test::Database> driver for C<DBD::SQLite2>.
+
+=head1 SEE ALSO
+
+L<Test::Database::Driver>
+
+=head1 AUTHOR
+
+Philippe Bruhat (BooK), C<< <book@cpan.org> >>
+
+=head1 COPYRIGHT
+
+Copyright 2008-2010 Philippe Bruhat (BooK), all rights reserved.
+
+=head1 LICENSE
+
+This module is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+=cut
+
diff --git a/lib/Test/Database/Driver/mysql.pm b/lib/Test/Database/Driver/mysql.pm
new file mode 100644
index 0000000..e112fc8
--- /dev/null
+++ b/lib/Test/Database/Driver/mysql.pm
@@ -0,0 +1,90 @@
+package Test::Database::Driver::mysql;
+use strict;
+use warnings;
+
+use DBI;
+
+use Test::Database::Driver;
+our @ISA = qw( Test::Database::Driver );
+
+sub _version {
+ return DBI->connect( $_[0]->connection_info() )
+ ->selectcol_arrayref('SELECT VERSION()')->[0];
+}
+
+sub create_database {
+ my ( $self ) = @_;
+ my $dbname = $self->available_dbname();
+
+ DBI->connect_cached( $self->connection_info() )
+ ->do("CREATE DATABASE $dbname");
+
+ # return the handle
+ return Test::Database::Handle->new(
+ dsn => $self->dsn($dbname),
+ name => $dbname,
+ username => $self->username(),
+ password => $self->password(),
+ driver => $self,
+ );
+}
+
+sub drop_database {
+ my ( $self, $dbname ) = @_;
+
+ DBI->connect_cached( $self->connection_info() )
+ ->do("DROP DATABASE $dbname")
+ if grep { $_ eq $dbname } $self->databases();
+}
+
+sub databases {
+ my ($self) = @_;
+ my $basename = qr/^@{[$self->_basename()]}/;
+ my $databases = eval {
+ DBI->connect_cached( $self->connection_info(), { PrintError => 0 } )
+ ->selectall_arrayref('SHOW DATABASES');
+ };
+ return grep {/$basename/} map {@$_} @$databases;
+}
+
+'mysql';
+
+__END__
+
+=head1 NAME
+
+Test::Database::Driver::mysql - A Test::Database driver for mysql
+
+=head1 SYNOPSIS
+
+ use Test::Database;
+ my @handles = Test::Database->handles( 'mysql' );
+
+=head1 DESCRIPTION
+
+This module is the C<Test::Database> driver for C<DBD::mysql>.
+
+=head1 SEE ALSO
+
+L<Test::Database::Driver>
+
+=head1 AUTHOR
+
+Philippe Bruhat (BooK), C<< <book@cpan.org> >>
+
+=head1 ACKNOWLEDGEMENTS
+
+Many thanks to Kristian Köhntopp who helped me while writing a
+previous version of this module (before C<Test::Database> 0.03).
+
+=head1 COPYRIGHT
+
+Copyright 2008-2010 Philippe Bruhat (BooK), all rights reserved.
+
+=head1 LICENSE
+
+This module is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+=cut
+
diff --git a/lib/Test/Database/Handle.pm b/lib/Test/Database/Handle.pm
new file mode 100644
index 0000000..dc8acd2
--- /dev/null
+++ b/lib/Test/Database/Handle.pm
@@ -0,0 +1,147 @@
+package Test::Database::Handle;
+use strict;
+use warnings;
+use Carp;
+use DBI;
+
+# basic accessors
+for my $attr (qw( dbd dsn username password name driver )) {
+ no strict 'refs';
+ *{$attr} = sub { return $_[0]{$attr} };
+}
+
+sub new {
+ my ( $class, %args ) = @_;
+
+ exists $args{$_} or croak "$_ argument required"
+ for qw( dsn );
+
+ my ( $scheme, $driver, $attr_string, $attr_hash, $driver_dsn )
+ = DBI->parse_dsn( $args{dsn} );
+
+ # fix args
+ %args = (
+ username => '',
+ password => '',
+ %args,
+ dbd => $driver,
+ );
+
+ # try to provide a Test::Database::Driver object
+ if ( !exists $args{driver} ) {
+ eval {
+ $args{driver} = "Test::Database::Driver::$driver"->new(
+ driver_dsn => $args{dsn},
+ username => $args{username},
+ password => $args{password},
+ );
+ };
+ }
+
+ return bless { %args }, $class;
+}
+
+sub connection_info { return @{ $_[0] }{qw( dsn username password )} }
+
+sub dbh {
+ my ( $self, $attr ) = @_;
+ return $self->{dbh} ||= DBI->connect( $self->connection_info(), $attr );
+}
+
+'IDENTITY';
+
+__END__
+
+=head1 NAME
+
+Test::Database::Handle - A class for Test::Database handles
+
+=head1 SYNOPSIS
+
+ use Test::Database;
+
+ my $handle = Test::Database->handle(@requests);
+ my $dbh = $handle->dbh();
+
+=head1 DESCRIPTION
+
+C<Test::Database::Handle> is a very simple class for encapsulating the
+information about a test database handle.
+
+C<Test::Database::Handle> objects are used within a test script to
+obtain the necessary information about a test database handle.
+Handles are obtained through the C<< Test::Database->handles() >>
+or C<< Test::Database->handle() >> methods.
+
+=head1 METHODS
+
+C<Test::Database::Handle> provides the following methods:
+
+=over 4
+
+=item new( %args )
+
+Return a new C<Test::Database::Handle> with the given parameters
+(C<dsn>, C<username>, C<password>).
+
+The only mandatory argument is C<dsn>.
+
+=back
+
+The following accessors are available.
+
+=over 4
+
+=item dsn()
+
+Return the Data Source Name.
+
+=item username()
+
+Return the connection username.
+
+=item password()
+
+Return the connection password.
+
+=item connection_info()
+
+Return the connection information triplet (C<dsn>, C<username>, C<password>).
+
+=item dbh( [ $attr ] )
+
+Return the DBI database handle obtained when connecting with the
+connection triplet returned by C<connection_info()>.
+
+The optional parameter C<$attr> is a reference to a hash of connection
+attributes, passed directly to DBI's C<connect()> method.
+
+=item name()
+
+Return the database name attached to the handle.
+
+=item dbd()
+
+Return the DBI driver name, as computed from the C<dsn>.
+
+=item driver()
+
+Return the C<Test::Database::Driver> object attached to the handle.
+
+=back
+
+=head1 AUTHOR
+
+Philippe Bruhat (BooK), C<< <book@cpan.org> >>
+
+=head1 COPYRIGHT
+
+Copyright 2008-2010 Philippe Bruhat (BooK), all rights reserved.
+
+=head1 LICENSE
+
+This module is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+=cut
+
diff --git a/lib/Test/Database/Tutorial.pod b/lib/Test/Database/Tutorial.pod
new file mode 100644
index 0000000..becb608
--- /dev/null
+++ b/lib/Test/Database/Tutorial.pod
@@ -0,0 +1,367 @@
+=head1 NAME
+
+Test::Database::Tutorial - How to use Test::Database
+
+=head1 INTRODUCTION
+
+The goal of the C<Test::Database> module is to provide easy to use test
+databases for test scripts that need them.
+
+=head2 The problem
+
+Until now, when a test script needed a database, it either used SQLite
+(or some other easy to setup database), or required some environment
+variables to be present, or used default credentials, or even set up
+the database by itself.
+
+Most of those methods have pros and cons:
+
+=over 4
+
+=item *
+
+using SQLite
+
+No setup needed, but the test script can only use SQLite's dialect of SQL.
+So much for portability across database engines.
+
+=item *
+
+using environment variables
+
+The environment variables are different for every module to test, and
+usually only the main developers/testers know about them. Since most of
+the CPAN testers probably don't bother setting them up, these modules
+are most certainly undertested.
+
+=item *
+
+using default credentials
+
+Typically using C<'root'> and C<''> to connect to the C<test> MySQL
+database, these test script assume a default installation on the host
+system. These credentials often provide full access to the database
+engine, which is a security risk in itself (see below).
+
+=item *
+
+setting up the database by itself
+
+This method usually uses the default credentials to access an account
+with enough privileges to create a database. The host system data may
+be at risk!
+
+=back
+
+
+=head2 A solution: C<Test::Database>
+
+Many modules use a database to store their data, and often support
+several database engines.
+
+Wouldn't it be nice to be able to test on all the supported databases
+that are available on the test system? Without breaking (into) anything?
+
+This is the goal of the C<Test::Database> module. It supports:
+
+=over 4
+
+=item *
+
+getting DSN information from a list of pre-configured database and engines
+
+=item *
+
+automatic detection of "file-based" database engines (typically, SQLite).
+
+=back
+
+The rest of this document describes various use cases for C<Test::Database>.
+
+
+=head1 MODULE AND TEST AUTHOR
+
+C<Test::Database> has a single interface for test authors:
+
+ my @handles = Test::Database->handles( @requests );
+
+C<@request> is a list of "requests" for databases handles. Requests
+must declare the DBD they expect, and can optionaly add version-based
+limitations (only available for drivers supported by C<Test::Database>).
+
+The handles returned are objects of the C<Test::Database::Handle> class.
+
+The data contained in the database is never destroyed or cleaned
+up by C<Test::Database>, so it's perfectly fine to have a startup script
+that will setup the necessary tables and test data, several tests scripts
+that will build and update the data, and a eventually a teardown script
+that will drop all created tables.
+
+C<Test::Database> can return two types of databases handles:
+
+=over 4
+
+=item *
+
+either a handle to a newly created database (created especially at the
+test script's request)
+
+=item *
+
+or a handle to an already existing database
+
+=back
+
+There is no way for the test script to tell the difference.
+
+In any case, the database is assumed to provide C<DROP TABLE> and
+C<CREATE TABLE> rights, and the test script is by definition allowed
+to do whatever it pleases with the tables that exist in the database.
+
+Note that C<Test::Database> supports any DSN, not just those for which
+it has a driver. If your module supports Oracle, you can add C<'Oracle'>
+to your list of requests, and if the host owner configured a C<dsn>
+pointing at an Oracle database, then it will be available for your tests.
+
+=head2 Specific database support
+
+It is possible to request specific versions of a database engine.
+
+ use Test::Database;
+
+ # request database handles for all available databases
+ my @handles = Test::Database->handles();
+
+ # or for only the databases we support
+ my @handles = Test::Database->handles(
+ { dbd => 'SQLite' },
+ { dbd => 'SQLite2' },
+ { dbd => 'mysql', min_version => '4.0' },
+ );
+
+See L<Test::Database> documentation for details about how
+to write a request.
+
+=head2 Testing on a development box
+
+The first systems on which you are going to test your module are the
+ones you own. On these system, it's up to you to configure the databases
+you want to make available.
+
+A typical F<~/.test-database> configuration file would look like this:
+
+ dsn = dbi:mysql:database=test
+ username = root
+
+ dsn = dbi:Pg:database=test
+ username = postgres
+
+ dsn = dbi:Oracle:test
+
+There is no need to add C<dsn> sections for file-based drivers
+(at least the ones that have a corresponding C<Test::Database::Driver>),
+since the module will automatically detect the available ones and create
+databases as needed.
+
+To find out which of the DBD that C<Test::Database> supports are
+installed, use the following one-liner:
+
+ $ perl -MTest::Database -le 'print for Test::Database->list_drivers("available")'
+ DBM
+ SQLite
+ mysql
+
+With no parameter, it will return the list of configured ones:
+
+ $ perl -MTest::Database -le 'print for Test::Database->list_drivers()'
+ DBM
+ SQLite
+
+
+=head1 CPAN TESTER
+
+The main goal of C<Test::Database> from the point of view of a tester
+is: "configure once, test everything".
+
+As a CPAN tester, once you have installed C<Test::Database>, you
+should edit the local equivalent of F<~/.test-database> for the
+user that will be running the CPAN test suites.
+
+=head2 C<dsn> versus C<driver_dsn>
+
+C<dsn> sections define the information needed to connect to a single
+database. Any database listed here can be used by any test script that
+requests it.
+
+C<driver_dsn> sections define the information needed to connect to a
+database engine (a "driver") with sufficient rights to run a
+C<CREATE DATABASE> command. This allows C<Test::Database> to create the
+databases on demand, thus ensuring every test suite will get a specific
+database.
+
+If you have file-based database engine, there is nothing to setup, as
+C<Test::Database> is able to detect available file-based engines and
+use them as needed.
+
+Other database engines like C<mysql> and C<Pg> require a little more
+configuration. For example, here's the content of my F<~/.test-database>
+configuration file:
+
+ driver_dsn = dbi:mysql:
+ username = root
+
+ driver_dsn = dbi:Pg:
+ username = postgres
+
+For C<Pg>, I had to edit the F<pg_hba.cong> file in F</etc> to make sure
+anyone would be able to connect as the C<postgres> user, for example.
+
+=head2 Several test hosts accessing the same database engine
+
+If you have a large scale testing setup, you may want to setup a single
+MySQL or Postgres instance for all your test hosts, rather than one per
+test host.
+
+Databases created by C<Test::Database::Driver> (using a configured
+C<driver_dsn> have a name built after the following template:
+C<tdd_I<driver>_I<login>_I<n>>, where I<driver> is the DBD name, I<login>
+is the login of the user running C<Test::Database> and I<n> a number that
+
+If the same database server is used by several host running
+C<Test::Database> from the same user account, there is a race condition
+during with two different host may try to create the a database with
+the same name. A simple trick to avoid this is to add a C<key> section
+to the F<~/.test-database> configuration file.
+
+If the C<key> entry exists, the template used by C<Test::Database::Driver>
+to create new databases is C<tdd_I<driver>_I<login>_I<key>_I<n>>.
+
+=head2 Cleaning the test drivers
+
+When given a C<driver_dsn>, C<Test::Database> will use it to create a
+database for each test suite that requests one. Some mapping information
+is created to ensure the same test suite always receives a handle to
+the same database. (The mapping of test suite to database is based on
+the current working directory when C<Test::Database> is loaded).
+
+After a while, your database engine may fill up with unused test databases.
+
+All drivers store their mapping information in the system's temporary
+directory, so the mapping information is relatively volatile, which implies
+more unused test databases (at least for non file-based drivers, since the
+file-based drivers store their database files in the system's temporary
+directory too).
+
+The following one-liner will list all the existing databases that were
+created by C<Test::Database> in your configured drivers:
+
+ perl -MTest::Database -le 'print join "\n ", $_->name, $_->databases for Test::Database->drivers'
+
+Example output:
+
+ CSV
+ tdd_csv_book_0
+ tdd_csv_book_1
+ DBM
+ SQLite
+ tdd_sqlite_book_0
+ tdd_sqlite_book_1
+ SQLite2
+ tdd_sqlite2_book_0
+ mysql
+ tdd_mysql_book_0
+ tdd_mysql_book_1
+
+The following one-liner will drop them all:
+
+ perl -MTest::Database -le 'for$d(Test::Database->drivers){$d->drop_database($_)for$d->databases}'
+
+If a C<key> has been defined in the configuration, only the databases
+corresponding to that key will be dropped.
+
+=head1 ADDING SUPPORT FOR A NEW DATABASE ENGINE
+
+C<Test::Database> currently supports the following DBD drivers:
+C<CSV>, C<DBM>, C<mysql>, C<Pg>, C<SQLite2>, C<SQLite>.
+
+Adding a new driver requires writing a corresponding
+C<Test::Database::Driver> subclass, having the same name as the original
+C<DBD> driver.
+
+An example module is provided in F<eg/MyDriver.pm>, and the other
+drivers can also be used as an example. See also the I<WRITING A
+DRIVER FOR YOUR DATABASE OF CHOICE> section in the documentation for
+C<Test::Database::Driver>.
+
+
+=head1 WHERE DO DSN COME FROM?
+
+The following ASCII-art graph shows where the C<Test::Database::Handle>
+objects returned by the C<handles()> method come from:
+
+
+ ,-------------, ,-------------, ,--------------,
+ | DSN from | | File-based | | Drivers from |
+ | config file | | drivers | | config file |
+ '-------------' '-------------' '--------------'
+ | | |
+ | | ,-----------, |
+ | '--->| Available |<----'
+ | | drivers |
+ | '-----------'
+ | |
+ | ,-----------, |
+ '------------->| Available |<--'
+ | DSN |
+ '-----------'
+
+
+Here are a few details about the C<handles()> method works:
+
+=over 4
+
+=item *
+
+C<Test::Database> maintains a list of C<Test::Database::Handle> objects
+computed from the DSN listed in the configuration.
+
+The handles matching the request are selected.
+
+=item *
+
+C<Test::Database> also maintains a list of C<Test::Database::Driver>
+objects computed from the list of supported file-based drivers that are
+locally available and from the list in the configuration file.
+
+The list of matching drivers is computed from the requests. Each driver
+is then requested to provide an existing database (using its existing
+mapping information) or to create one if needed, and returns the
+corresponding C<Test::Database::Handle> objects.
+
+=item *
+
+Finally, all the collected C<Test::Database::Handle> objects are returned.
+
+=back
+
+So, without any configuration, C<Test::Database> will only be able to
+provide file-based databases. It is also recommended to B<not> put DSN
+or driver information for the file-based database engines that have
+a corresponding C<Test::Database::Driver> class, since it will cause
+C<handles()> to return several handles for the same database engine.
+
+=head1 AUTHOR
+
+Philippe Bruhat (BooK), C<< <book@cpan.org> >>
+
+=head1 COPYRIGHT
+
+Copyright 2009-2010 Philippe Bruhat (BooK), all rights reserved.
+
+=head1 LICENSE
+
+You can redistribute this tutorial and/or modify it under the same terms
+as Perl itself.
+
+=cut
+
diff --git a/lib/Test/Database/Util.pm b/lib/Test/Database/Util.pm
new file mode 100644
index 0000000..efb3938
--- /dev/null
+++ b/lib/Test/Database/Util.pm
@@ -0,0 +1,98 @@
+package Test::Database::Util;
+use strict;
+use warnings;
+use Carp;
+
+# export everything
+sub import {
+ my $caller = caller();
+ no strict 'refs';
+ *{"${caller}::$_"} = \&$_ for qw( _read_file );
+}
+
+# return a list of hashrefs representing each configuration section
+sub _read_file {
+ my ($file) = @_;
+ my @config;
+
+ open my $fh, '<', $file or croak "Can't open $file for reading: $!";
+ my $re_header = qr/^(?:(?:driver_)?dsn|key)$/;
+ my %args;
+ my $records;
+ while (<$fh>) {
+ next if /^\s*(?:#|$)/; # skip blank lines and comments
+ chomp;
+
+ /\s*(\w+)\s*=\s*(.*)\s*/ && do {
+ my ( $key, $value ) = ( $1, $2 );
+ if ( $key =~ $re_header ) {
+ push @config, {%args} if keys %args;
+ $records++;
+ %args = ();
+ }
+ elsif ( !$records ) {
+ croak "Record doesn't start with dsn or driver_dsn or key "
+ . "at $file, line $.:\n <$_>";
+ }
+ $args{$key} = $value;
+ next;
+ };
+
+ # unknown line
+ croak "Can't parse line at $file, line $.:\n <$_>";
+ }
+ push @config, {%args} if keys %args;
+ close $fh;
+
+ return @config;
+}
+
+'USING';
+
+__END__
+
+=head1 NAME
+
+Test::Database::Util - Utility functions for Test::Database modules
+
+=head1 SYNOPSIS
+
+ use Test::Database::Util;
+
+ # exports a collection of underscore functions
+
+=head1 DESCRIPTION
+
+C<Test::Database::Util> exports a collection of functions used by
+several modules in the C<Test-Database> distribution.
+
+=head1 EXPORTED FUNCTIONS
+
+All functions provided by C<Test::Database::Util> are exported in the
+calling package.
+
+The following functions are provided:
+
+=over 4
+
+=item _read_file( $file )
+
+Return a list of hash references, read in the given C<$file> file.
+
+=back
+
+=head1 AUTHOR
+
+Philippe Bruhat (BooK), C<< <book@cpan.org> >>
+
+=head1 COPYRIGHT
+
+Copyright 2008-2010 Philippe Bruhat (BooK), all rights reserved.
+
+=head1 LICENSE
+
+This module is free software; you can redistribute it and/or modify it
+under the same terms as Perl itself.
+
+=cut
+
diff --git a/t/00-load.t b/t/00-load.t
new file mode 100644
index 0000000..43061b9
--- /dev/null
+++ b/t/00-load.t
@@ -0,0 +1,16 @@
+use strict;
+use warnings;
+use Test::More;
+use File::Find;
+
+my @modules;
+find( sub { push @modules, $File::Find::name if /\.pm$/ }, 'blib/lib' );
+
+plan tests => scalar @modules;
+
+use_ok($_)
+ for reverse sort map { s!/!::!g; s/\.pm$//; s/^blib::lib:://; $_ }
+ @modules;
+
+diag("Tested Test::Database $Test::Database::VERSION, Perl $], $^X");
+
diff --git a/t/08-handle.t b/t/08-handle.t
new file mode 100644
index 0000000..474fe01
--- /dev/null
+++ b/t/08-handle.t
@@ -0,0 +1,51 @@
+use strict;
+use warnings;
+use Test::More;
+use Test::Database::Handle;
+use List::Util qw( sum );
+
+my @tests = (
+
+ # args, expected result, error regex
+ [ [], undef, qr/^dsn argument required/ ],
+ [ [ dbd => 'Zlonk' ], undef, qr/^dsn argument required/ ],
+ [ [ driver => 'Foo', dsn => 'dbi:SQLite:dbname=zlonk' ],
+ { dsn => 'dbi:SQLite:dbname=zlonk',
+ username => '',
+ password => '',
+ dbd => 'SQLite',
+ driver => 'Foo',
+ }
+ ],
+ [ [ dbd => 'SQLite',
+ dsn => 'dbi:SQLite:dbname=zlonk',
+ name => 'zlonk'
+ ],
+ { dsn => 'dbi:SQLite:dbname=zlonk',
+ username => '',
+ password => '',
+ dbd => 'SQLite',
+ name => 'zlonk',
+ }
+ ],
+);
+my @attr = qw( dsn username password dbd );
+
+plan tests => sum map { $_->[2] ? 1 : 1 + @attr } @tests;
+
+for my $t (@tests) {
+ my ( $args, $expected, $err ) = @$t;
+
+ my $got = eval { Test::Database::Handle->new(@$args) };
+ my $call = "Test::Database::Handle->new( "
+ . join( ', ', map {"'$_'"} @$args ) . " )";
+
+ if ($@) {
+ like( $@, $err, "Expected error message for $call" );
+ }
+ else {
+ isa_ok( $got, 'Test::Database::Handle' );
+ is( $got->$_, $expected->{$_}, "$_ for $call" ) for @attr;
+ }
+}
+
diff --git a/t/09-handle-dsn.t b/t/09-handle-dsn.t
new file mode 100644
index 0000000..91237fc
--- /dev/null
+++ b/t/09-handle-dsn.t
@@ -0,0 +1,45 @@
+use strict;
+use warnings;
+use Test::More;
+use Test::Database::Handle;
+
+use DBI;
+use File::Spec;
+use File::Temp qw( tempdir );
+
+my $dir = tempdir( CLEANUP => 1 );
+my $db = File::Spec->catfile( $dir, 'db.sqlite' );
+
+my $dsn = "dbi:SQLite:$db";
+my $dbh;
+eval { $dbh = DBI->connect($dsn) }
+ or plan skip_all => 'DBD::SQLite needed for this test';
+
+# some SQL statements to try out
+my @sql = (
+ q{CREATE TABLE users (id INTEGER, name VARCHAR(64))},
+ q{INSERT INTO users (id, name) VALUES (1, 'book')},
+ q{INSERT INTO users (id, name) VALUES (2, 'echo')},
+);
+my $select = "SELECT id, name FROM users";
+
+plan tests => @sql + 4;
+
+# create some information
+ok( $dbh->do($_), $_ ) for @sql;
+
+# create handle
+my $handle = Test::Database::Handle->new( dsn => $dsn );
+
+is_deeply(
+ [ $handle->connection_info() ],
+ [ $dsn, '', '' ],
+ 'connection_info()'
+);
+isa_ok( my $dbh2 = $handle->dbh(), 'DBI::db' );
+cmp_ok( $handle->dbh(), 'eq', $dbh2, 'cached dbh' );
+
+# check the data is there
+my $lines = $dbh->selectall_arrayref($select);
+is_deeply( $lines, [ [ 1, 'book' ], [ 2, 'echo' ] ], $select );
+
diff --git a/t/10-drivers.t b/t/10-drivers.t
new file mode 100644
index 0000000..37845c9
--- /dev/null
+++ b/t/10-drivers.t
@@ -0,0 +1,106 @@
+use strict;
+use warnings;
+use Test::More;
+use Test::Database;
+use Test::Database::Driver;
+
+# for file-based drivers, the dbd parameter is enough
+# but for other drivers, we'll need the driver_dsn, username and password
+my @drivers = (
+ map {
+ my $d = $_;
+ +{ map { $_ => $d->{$_} }
+ grep { exists $d->{$_} }
+ qw( driver_dsn dbd username password ) }
+ } Test::Database->drivers()
+);
+
+plan tests => 5 + @drivers * ( 1 + 2 * 12 ) + 2;
+
+my $base = 'Test::Database::Driver';
+
+# tests for Test::Database::Driver directly
+{
+ ok( !eval { Test::Database::Driver->new(); 1 },
+ 'Test::Database::Driver->new() failed'
+ );
+ like(
+ $@,
+ qr/^dbd or driver_dsn parameter required at/,
+ 'Expected error message'
+ );
+ my $dir = $base->base_dir();
+ ok( $dir, "$base has a base_dir(): $dir" );
+ like( $dir, qr/Test-Database-.*/,
+ "$base\'s base_dir() looks like expected" );
+ ok( -d $dir, "$base base_dir() is a directory" );
+}
+
+# now test the subclasses
+
+for my $args (@drivers) {
+ my $name = $args->{dbd};
+ my $class = "Test::Database::Driver::$name";
+ use_ok($class);
+
+ for my $t (
+ [ $base => eval { $base->new(%$args) } || ( '', $@ ) ],
+ [ $class => eval { $class->new(%$args) } || ( '', $@ ) ],
+ )
+ {
+ my ( $created_by, $driver, $at ) = @$t;
+ $at =~ s/ at .*\n// if $at;
+ SKIP: {
+ skip "Failed to create $name driver with $created_by ($at)", 12
+ if !$driver;
+ diag "$name driver (created by $created_by)";
+
+ # class and name
+ my $desc = "$name driver";
+ isa_ok( $driver, $class, $desc );
+ is( $driver->name(), $name, "$desc has the expected name()" );
+
+ # base_dir
+ my $dir = $driver->base_dir();
+ ok( $dir, "$desc has a base_dir(): $dir" );
+ like( $dir, qr/Test-Database-.*\Q$name\E/,
+ "$desc\'s base_dir() looks like expected" );
+ ok( -d $dir, "$desc base_dir() is a directory" );
+
+ # version
+ my $version;
+ ok( eval { $version = $driver->version() },
+ "$desc has a version(): $version"
+ );
+ diag $@ if $@;
+ isa_ok( $version, 'version', "$desc version()" );
+
+ # version_dbd
+ my $version_dbd;
+ ok( eval { $version_dbd = $driver->dbd_version() },
+ "$desc has a dbd_version(): $version_dbd"
+ );
+ diag $@ if $@;
+
+ # driver_dsn, username, password, connection_info
+ ok( $driver->driver_dsn(), "$desc has a driver_dsn()" );
+ ok( defined $driver->username(), "$desc has a username()" );
+ ok( defined $driver->password(), "$desc has a password()" );
+ is_deeply(
+ [ $driver->connection_info() ],
+ [ map { $driver->$_ } qw< driver_dsn username password > ],
+ "$desc has a connection_info()"
+ );
+ }
+ }
+}
+
+# get all loaded drivers
+@drivers = Test::Database->list_drivers();
+cmp_ok( scalar @drivers, '>=', 1, 'At least one driver loaded' );
+
+# unload them
+Test::Database->clean_config();
+@drivers = Test::Database->list_drivers();
+is( scalar @drivers, 0, 'All drivers were unloaded' );
+
diff --git a/t/10-list_drivers.t b/t/10-list_drivers.t
new file mode 100644
index 0000000..b7aa77f
--- /dev/null
+++ b/t/10-list_drivers.t
@@ -0,0 +1,33 @@
+use strict;
+use warnings;
+use Test::More;
+use Test::Database;
+
+# hardcoded sorted list of our drivers
+my @all_drivers = sort qw( CSV DBM Pg SQLite SQLite2 mysql );
+
+# intersection with DBI->available_drivers
+my %all_drivers = map { $_ => 1 } @all_drivers;
+my @available_drivers
+ = sort grep { exists $all_drivers{$_} } DBI->available_drivers;
+
+plan tests => 3;
+
+# minimal setup
+Test::Database->clean_config();
+Test::Database->load_drivers();
+
+# existing Test::Database::Driver:: drivers
+is_deeply( [ Test::Database->list_drivers('all') ],
+ \@all_drivers, q{list_drivers('all')} );
+
+# available DBI drivers
+is_deeply( [ Test::Database->list_drivers('available') ],
+ \@available_drivers, q{list_drivers('available')} );
+
+# available DBI drivers we could load (should only be file-based)
+my @filebased
+ = grep { "Test::Database::Driver::$_"->is_filebased() } @available_drivers;
+is_deeply( [ Test::Database->list_drivers() ], \@filebased,
+ 'list_drivers()' );
+
diff --git a/t/11-available_dbname.t b/t/11-available_dbname.t
new file mode 100644
index 0000000..57e180e
--- /dev/null
+++ b/t/11-available_dbname.t
@@ -0,0 +1,44 @@
+use strict;
+use warnings;
+use Test::More;
+use Test::Database::Driver;
+
+# fake the databases() method
+my @db;
+{
+ no strict;
+ @{"Test::Database::Driver::Zlonk::ISA"} = qw( Test::Database::Driver );
+ *{"Test::Database::Driver::Zlonk::databases"} = sub {@db};
+}
+
+# our test plans
+my @names = ( 0, 1, 3, 2, 4 );
+my @expected = ( 0, 1, 2, 2, 4, 5 );
+
+plan tests => 4 + @expected;
+
+# check the basename
+like( Test::Database::Driver::Zlonk->_basename(),
+ qr/^tdd_zlonk_\w+_$/, "_basename looks correct" );
+
+# test _set_key
+my $bad = 'a b c';
+ok( !eval { Test::Database::Driver->_set_key($bad); 1 }, "Bad key: $bad" );
+like( $@, qr/^Invalid format for key '$bad' at/, 'Expected error message' );
+
+# set a correct key
+Test::Database::Driver->_set_key('clunk');
+like( Test::Database::Driver::Zlonk->_basename(),
+ qr/^tdd_zlonk_\w+_clunk_$/, "_basename looks correct (with key)" );
+
+# now correctly compute our expectations
+my $dbname = Test::Database::Driver::Zlonk->_basename();
+@names = map {"$dbname$_"} @names;
+@expected = map {"$dbname$_"} @expected;
+
+for my $expected (@expected) {
+ is( Test::Database::Driver::Zlonk->available_dbname(),
+ $expected, "available_dbname() = $expected" );
+ push @db, shift @names;
+}
+
diff --git a/t/11-make_dsn.t b/t/11-make_dsn.t
new file mode 100644
index 0000000..7d56afd
--- /dev/null
+++ b/t/11-make_dsn.t
@@ -0,0 +1,40 @@
+use strict;
+use warnings;
+use Test::More;
+use Test::Database::Driver;
+use version;
+
+# test version_matches() on a dummy driver
+
+my @tests = (
+ [ '', 'dbi:Dummy:' ],
+ [ '', 'dbi:Dummy:bam=boff', qw( bam boff ) ],
+ [ '', 'dbi:Dummy:bam=boff;z_zwap=plop', qw( bam boff z_zwap plop ) ],
+ [ 'dbi:Dummy:bam=boff', 'dbi:Dummy:bam=boff;z_zwap=plop',
+ qw( z_zwap plop )
+ ],
+ [ 'dbi:Dummy:bam=boff',
+ 'dbi:Dummy:bam=boff;z_zwap=plop;zowie=sock',
+ qw( z_zwap plop zowie sock )
+ ],
+);
+
+@Test::Database::Driver::Dummy::ISA = qw( Test::Database::Driver );
+
+plan tests => scalar @tests;
+
+for my $t (@tests) {
+ my ( $driver_dsn, $dsn, @args ) = @$t;
+ my $driver = bless { driver_dsn => $driver_dsn },
+ 'Test::Database::Driver::Dummy';
+
+ my $got = $driver->make_dsn(@args);
+ is( $got, $dsn, $driver->driver_dsn() . ' ' . to_string(@args) );
+}
+
+sub to_string {
+ my %args = @_;
+ return
+ '( ' . join( ', ', map {"$_ => $args{$_}"} sort keys %args ) . ' )';
+}
+
diff --git a/t/11-version_matches.t b/t/11-version_matches.t
new file mode 100644
index 0000000..5e64ea3
--- /dev/null
+++ b/t/11-version_matches.t
@@ -0,0 +1,88 @@
+use strict;
+use warnings;
+use Test::More;
+use Test::Database::Driver;
+use version;
+
+# test version_matches() on a dummy driver
+
+my @requests;
+
+my @ok = (
+ {},
+ { version => '1.2.3' },
+ { min_version => '1.2.2' },
+ { min_version => '1.2.3' },
+ { max_version => '1.3.0' },
+ { version => '1.2.3', min_version => '1.2.0' },
+ { version => '1.2.3', max_version => '1.4.3' },
+ { min_version => '1.2.0', max_version => '2.0' },
+ { version => '1.2.3', min_version => '1.2.0', max_version => '2.0' },
+ { regex_version => qr/^1\.2/ },
+);
+
+my @ok_beta
+ = map { my %r = %$_; $r{version} = '1.2.3-beta' if $r{version}; \%r } @ok;
+push @ok_beta, { regex_version => qr/beta/ };
+
+my @not_ok = (
+ { min_version => '1.3.0' },
+ { max_version => '1.002' },
+ { max_version => '1.2.3' },
+ { version => '1.2.3-beta' },
+ { version => '1.3.4' },
+ { min_version => '1.3.0', max_version => '2.1' },
+ { min_version => '0.1.3', max_version => '1.002' },
+ { regex_version => qr/^1\.2\.[1245]$/ },
+ { regex_version => qr/^1\.2$/ },
+);
+
+my @not_ok_beta = map {
+ my %r = %$_;
+ $r{version} = '1.2.3' if $r{version} && $r{version} eq '1.2.3-beta';
+ \%r
+} @not_ok;
+
+# define our dummy class
+package Test::Database::Driver::Dummy;
+our @ISA = qw( Test::Database::Driver );
+sub _version { $_[0]{xxx} || '1.2.3' }
+
+package main;
+my $driver = bless {}, 'Test::Database::Driver::Dummy';
+my $driver_beta = bless { xxx => '1.2.3-beta' },
+ 'Test::Database::Driver::Dummy';
+
+plan tests => @ok + @not_ok + @ok_beta + @not_ok_beta;
+
+for my $request (@ok) {
+ ok( $driver->version_matches($request),
+ to_string($request) . ' matches driver'
+ );
+}
+
+for my $request (@not_ok) {
+ ok( !$driver->version_matches($request),
+ to_string($request) . ' does not match driver' );
+}
+
+for my $request (@ok_beta) {
+ ok( $driver_beta->version_matches($request),
+ to_string($request) . ' matches beta driver'
+ );
+}
+
+for my $request (@not_ok_beta) {
+ ok( !$driver_beta->version_matches($request),
+ to_string($request) . ' does not match beta driver'
+ );
+}
+
+sub to_string {
+ my ($request) = @_;
+ return
+ '{ '
+ . join( ', ', map {"$_ => $request->{$_}"} sort keys %$request )
+ . ' }';
+}
+
diff --git a/t/12-load.t b/t/12-load.t
new file mode 100644
index 0000000..b18640c
--- /dev/null
+++ b/t/12-load.t
@@ -0,0 +1,56 @@
+use strict;
+use warnings;
+use Test::More;
+use Test::Database::Util;
+use File::Spec;
+
+my @good = (
+ { dsn => 'dbi:mysql:database=mydb;host=localhost;port=1234',
+ username => 'user',
+ password => 's3k r3t',
+ },
+ { dsn => 'dbi:mysql:database=mydb;host=remotehost;port=5678',
+ username => 'otheruser',
+ },
+ { dsn => 'dbi:SQLite:db.sqlite' },
+ { driver_dsn => 'dbi:mysql:host=remotehost;port=5678',
+ username => 'otheruser',
+ },
+);
+
+my @bad = (
+ [ File::Spec->catfile(qw< t database.bad >),
+ qr/^Can't parse line at .*, line \d+:\n <bad format> at /
+ ],
+ [ File::Spec->catfile(qw< t database.bad2 >),
+ qr/^Record doesn't start with dsn or driver_dsn .*, line \d+:\n <drh = dbi:mysql:> at /
+ ],
+ [ 'missing', qr/^Can't open missing for reading: / ],
+);
+
+plan tests => 1 + @good + 2 * @bad + 1;
+
+# load a correct file
+my $file = File::Spec->catfile(qw< t database.good >);
+my @config = _read_file($file);
+
+is( scalar @config, scalar @good,
+ "Got @{[scalar @good]} handles from $file" );
+
+for my $test (@good) {
+ my $args = shift @config;
+ is_deeply( $args, $test,
+ "Read args for handle " . ( $test->{dsn} || $test->{driver_dsn} ) );
+}
+
+# try to load a bad file
+for my $t (@bad) {
+ my ( $file, $regex ) = @$t;
+ ok( !eval { _read_file($file); 1 }, "_read_file( $file ) failed" );
+ like( $@, $regex, 'Expected error message' );
+}
+
+# load an empty file
+$file = File::Spec->catfile(qw< t database.empty >);
+is( scalar _read_file($file), 0, 'Empty file' );
+
diff --git a/t/20-handles.t b/t/20-handles.t
new file mode 100644
index 0000000..be2fdbb
--- /dev/null
+++ b/t/20-handles.t
@@ -0,0 +1,106 @@
+use strict;
+use warnings;
+use Test::More;
+use File::Spec;
+use Test::Database;
+
+my %handle = (
+ mysql1 => Test::Database::Handle->new(
+ dsn => 'dbi:mysql:database=mydb;host=localhost;port=1234',
+ username => 'user',
+ password => 's3k r3t',
+ ),
+ mysql2 => Test::Database::Handle->new(
+ dsn => 'dbi:mysql:database=mydb;host=remotehost;port=5678',
+ username => 'otheruser',
+ ),
+ sqlite => Test::Database::Handle->new( dsn => 'dbi:SQLite:db.sqlite', ),
+);
+delete $_->{driver} for values %handle;
+
+# test description:
+# 1st char is variable to look at: array (@) or scalar ($)
+# 2nd char is expected result: list (@), single item ($) or number (1)
+my @code;
+my %tests = map {
+ my ( $fmt, $code ) = split / /, $_, 2;
+ push @code, $code;
+ ( $code => $fmt )
+} split /\n/, << 'CODE';
+@@ @handles = Test::Database->handles(@requests);
+$1 $handle = Test::Database->handles(@requests);
+$$ $handle = ( Test::Database->handles(@requests) )[0];
+$$ ($handle) = Test::Database->handles(@requests);
+$$ $handle = Test::Database->handle(@requests);
+@$ @handles = Test::Database->handle(@requests);
+CODE
+
+my @tests = (
+
+ # request, expected response
+ [ [], [ @handle{qw( mysql1 mysql2 sqlite )} ], '' ],
+ [ ['mysql'], [ @handle{qw( mysql1 mysql2 )} ], q{'mysql'} ],
+ [ ['sqlite'], [], q{'sqlite'} ],
+ [ ['SQLite'], [ $handle{sqlite} ], q{'SQLite'} ],
+ [ ['Oracle'], [], q{'Oracle'} ],
+ [ [ 'SQLite', 'mysql' ],
+ [ @handle{qw( mysql1 mysql2 sqlite )} ],
+ q{'SQLite', 'mysql'}
+ ],
+ [ [ 'mysql', 'SQLite', 'mysql' ],
+ [ @handle{qw( mysql1 mysql2 sqlite )} ],
+ q{'mysql', 'SQLite', 'mysql'}
+ ],
+ [ [ 'mysql', 'Oracle', 'SQLite' ],
+ [ @handle{qw( mysql1 mysql2 sqlite )} ],
+ q{'Oracle', 'mysql', 'SQLite'}
+ ],
+ [ [ { dbd => 'mysql' } ], [ @handle{qw( mysql1 mysql2 )} ], q{'mysql'} ],
+ [ [ { driver => 'mysql' } ],
+ [ @handle{qw( mysql1 mysql2 )} ],
+ q{'mysql'}
+ ],
+
+);
+
+# reset the internal structures and force loading our test config
+Test::Database->clean_config();
+my $config = File::Spec->catfile( 't', 'database.rc' );
+Test::Database->load_config( $config );
+
+plan tests => @tests * keys %tests;
+
+for my $test (@tests) {
+ my ( $requests, $responses, $desc ) = @$test;
+ my %expected = (
+ '1' => [ scalar @$responses ],
+ '$' => [ $responses->[0] ],
+ '@' => $responses,
+ '0' => [],
+ );
+
+ # try out each piece of code
+ my @requests = @$requests;
+ for my $code (@code) {
+ my ( $handle, @handles );
+ my ( $got, $expected ) = split //, $tests{$code};
+
+ # special case
+ $expected = '0' if $tests{$code} eq '@$' && !@$responses;
+
+ # run the code
+ eval "$code; 1;" or do {
+ ok( 0, $code );
+ diag $@;
+ next;
+ };
+ ( my $mesg = $code ) =~ s/\@requests/$desc/;
+ $got
+ = $got eq '$' ? [$handle]
+ : $got eq '@' ? \@handles
+ : die "Unknown variable symbol $got";
+ ref && delete $_->{driver} for @$got;
+ is_deeply( $got, $expected{$expected}, $mesg );
+ }
+}
+
diff --git a/t/25-sql.t b/t/25-sql.t
new file mode 100644
index 0000000..6f6d8d0
--- /dev/null
+++ b/t/25-sql.t
@@ -0,0 +1,86 @@
+use strict;
+use warnings;
+use Test::More;
+use File::Spec;
+
+# DBD::DBM uses SQL::Statement if available
+# but SQL::Statement versions > 1.20 make the test fail
+# (see RT #56463, #56561)
+BEGIN {
+ if ( eval { require SQL::Statement; $SQL::Statement::VERSION > 1.20; } ) {
+ $ENV{DBI_SQL_NANO} = 1;
+ }
+}
+
+use Test::Database;
+
+my @drivers = Test::Database->drivers();
+@drivers = grep {
+ my $name = $_->name();
+ grep { $name eq $_ } @ARGV
+} @drivers if @ARGV;
+
+plan skip_all => 'No drivers available for testing' if !@drivers;
+
+# some SQL statements to try out
+my @sql = (
+ q{CREATE TABLE users (id INTEGER, name VARCHAR(64))},
+ q{INSERT INTO users (id, name) VALUES (1, 'book')},
+ q{INSERT INTO users (id, name) VALUES (2, 'echo')},
+);
+my $select = "SELECT id, name FROM users";
+my $drop = 'DROP TABLE users';
+
+plan tests => ( 1 + ( 3 + @sql + 1 ) * 2 + 1 + 2) * @drivers;
+
+for my $driver (@drivers) {
+ my $drname = $driver->name();
+ diag "Testing driver $drname " . $driver->version()
+ . ", DBD::$drname " . $driver->dbd_version();
+ isa_ok( $driver, 'Test::Database::Driver' );
+
+ my $count = 0;
+ my $old;
+ for my $request (
+ $drname,
+ { dbd => $drname },
+ )
+ {
+
+ # database handle to a database (created by the driver)
+ my ($handle) = Test::Database->handles($request);
+ my $dbname = $handle->{name};
+ isa_ok( $handle, 'Test::Database::Handle', "$drname $dbname" );
+
+ # check we always get the same database, when it's created
+ is( $dbname, $old, "Got db $old again" ) if $old;
+ $old ||= $dbname;
+
+ # do some tests on the dbh
+ my $desc = "$drname($dbname)";
+ my $dbh = $handle->dbh();
+ isa_ok( $dbh, 'DBI::db' );
+
+ # create some information
+ ok( $dbh->do($_), "$desc: $_" ) for @sql;
+
+ # check the data is there
+ my $lines = $dbh->selectall_arrayref($select);
+ is_deeply(
+ $lines,
+ [ [ 1, 'book' ], [ 2, 'echo' ] ],
+ "$desc: $select"
+ );
+
+ # remove everything
+ ok( $dbh->do($drop), "$desc: $drop" );
+ $dbh->disconnect();
+ }
+
+ ok( grep ( { $_ eq $old } $driver->databases() ),
+ "Database $old still there" );
+ $driver->drop_database($old);
+ ok( !grep ( { $_ eq $old } $driver->databases() ),
+ "Database $old was dropped" );
+}
+
diff --git a/t/database.bad b/t/database.bad
new file mode 100644
index 0000000..b72ee06
--- /dev/null
+++ b/t/database.bad
@@ -0,0 +1,14 @@
+# example correct .test-database.rc file
+
+# mysql
+driver_dsn = mysql
+host = localhost
+username = root
+password = "s3k r3t"
+
+bad format
+
+driver = SQLite
+
+driver = CSV
+
diff --git a/t/database.bad2 b/t/database.bad2
new file mode 100644
index 0000000..e779a94
--- /dev/null
+++ b/t/database.bad2
@@ -0,0 +1,3 @@
+drh = dbi:mysql:
+username = root
+
diff --git a/t/database.empty b/t/database.empty
new file mode 100644
index 0000000..cc0dae7
--- /dev/null
+++ b/t/database.empty
@@ -0,0 +1,4 @@
+# example correct .test-database.rc file
+
+# but empty
+
diff --git a/t/database.good b/t/database.good
new file mode 100644
index 0000000..28b8a7d
--- /dev/null
+++ b/t/database.good
@@ -0,0 +1,17 @@
+# example correct .test-database.rc file
+
+# mysql
+dsn = dbi:mysql:database=mydb;host=localhost;port=1234
+username = user
+password = s3k r3t
+
+# another
+dsn = dbi:mysql:database=mydb;host=remotehost;port=5678
+username = otheruser
+
+# sqlite
+dsn = dbi:SQLite:db.sqlite
+
+# a database driver
+driver_dsn = dbi:mysql:host=remotehost;port=5678
+username = otheruser
diff --git a/t/database.rc b/t/database.rc
new file mode 100644
index 0000000..f6349c9
--- /dev/null
+++ b/t/database.rc
@@ -0,0 +1,14 @@
+# example correct .test-database.rc file
+
+# mysql
+dsn = dbi:mysql:database=mydb;host=localhost;port=1234
+username = user
+password = s3k r3t
+
+# another
+dsn = dbi:mysql:database=mydb;host=remotehost;port=5678
+username = otheruser
+
+# sqlite
+dsn = dbi:SQLite:db.sqlite
+
diff --git a/t/pod-coverage.t b/t/pod-coverage.t
new file mode 100644
index 0000000..4aec937
--- /dev/null
+++ b/t/pod-coverage.t
@@ -0,0 +1,40 @@
+use strict;
+use warnings;
+use Test::More;
+
+# Ensure a recent version of Test::Pod::Coverage
+my $min_tpc = 1.08;
+eval "use Test::Pod::Coverage $min_tpc";
+plan skip_all =>
+ "Test::Pod::Coverage $min_tpc required for testing POD coverage"
+ if $@;
+
+# Test::Pod::Coverage doesn't require a minimum Pod::Coverage version,
+# but older versions don't recognize some common documentation styles
+my $min_pc = 0.18;
+eval "use Pod::Coverage $min_pc";
+plan skip_all => "Pod::Coverage $min_pc required for testing POD coverage"
+ if $@;
+
+my @drivers;
+my @modules
+ = grep { $_ ne 'Test::Database' }
+ grep { !/Driver::/ or push @drivers, $_ and 0 } all_modules();
+
+plan tests => @modules + @drivers + 1;
+
+# Test::Database exports are not documented
+pod_coverage_ok( 'Test::Database', { trustme => [qr/^test_db_\w+$/] } );
+
+# no exception for those modules
+pod_coverage_ok($_) for @modules;
+
+# the drivers methods are documented Test::Database::Driver
+pod_coverage_ok(
+ $_,
+ { trustme => [
+ qr/^(?:(?:create|drop)_database|databases|dsn|is_filebased|cleanup|essentials)$/
+ ]
+ }
+) for @drivers;
+
diff --git a/t/pod.t b/t/pod.t
new file mode 100644
index 0000000..ee8b18a
--- /dev/null
+++ b/t/pod.t
@@ -0,0 +1,12 @@
+#!perl -T
+
+use strict;
+use warnings;
+use Test::More;
+
+# Ensure a recent version of Test::Pod
+my $min_tp = 1.22;
+eval "use Test::Pod $min_tp";
+plan skip_all => "Test::Pod $min_tp required for testing POD" if $@;
+
+all_pod_files_ok();