Show quoted text> > Test::Database (or perhaps the individual Drivers) should have a 'reset'
> > or 'clean' method to make sure that the database is empty.
>
> The method is called cleanup(), and be called like this:
>
> Test::Database->cleanup(); # clean the whole directory
> Test::Database::Driver->cleanup; # same as above
> Test::Database::Driver::Foo->cleanup; # clean only the driver's
> directory
Woops. Sorry, not reading the entire documentation before posting. But
I think we both came to the same conclusion about it not making sense
to have a global ->cleanup method.
Show quoted text> In a test suite, if one needs a clean database at the beginning, they'll
> only need to ask for it in the first script, then all the others will
> use the one that was setup first.
> I think it's more practical that using the default in the first script,
> and then explicitely request to *not* clean the database in the
> following ones.
You are right, I got this backwards.
Show quoted text> > For SQLite the cleanup is easy. For things like MySQL it becomes
> > something like $dbh->do('DROP TABLE * CASCADE') or whatever the syntax is.
> >
>
> In my opinion, the easiest way to cleanup the database that were fully
> created by Test::Database is to just drop the directory... Oh well,
> you're right: with SQLite, I'll drop every database. So maybe we need
> a way to drop a single database, by name.
>
> ...
>
> Actually, I think I'm going to discourage using named databases.
>
> Here's the rationale:
> - some databases are much harder to setup than SQLite or MySQL, and the
> only way to reasonably provide one to modules for their test suite is by
> having the machine owner (CPAN tester?) configure the available databases
I agree. But the same rationale says to me that start_server and
stop_server in Test::Database::Driver also shouldn't be part of this
distribution. The starting and stopping of database processes is very
operating system dependent, and not something that can easily be
defined in the (to be determined Test::Database::Config) configuration
file.
I know you've said you'll write down the use cases and reply, but I'm
in a different time zone so I'll raise the question now :-) What should
the interface between the various components (Author, Test::Database,
Tester) be? I think it goes like this:
Portable SQL Test Writer
-------------------------
If the SQL is very simple and likely to be extrememly portable, and the
author wants to check against everything available then she doesn't
need to specify any specific drivers:
# Case 1
foreach my $driver (Test::Database->drivers) {
# tests
}
Non-Portable SQL Test Writer
----------------------------
Author knows (or wants) that their code will only work against a
specific subset of drivers, so they are specified:
# Case 2
foreach my $driver (Test::Database->drivers('SQLite', 'mysql') {
# tests
}
Perhaps they want to include drivers specific to their local setup:
# Case 3
foreach my $driver (Test::Database->drivers('mysql', 'mysql4') {
# tests
}
Perhaps they could specify negation instead:
# Case 4
foreach my $driver (Test::Database->drivers('!DBM') {
# tests
}
Test::Database
--------------
At "use Test::Database" an internal drivers configuration is built
from:
1. File-system based installed DBD::*
over-written with:
2. Intersection of $HOME/.Test-Database and installed DBD::*
Default Case
------------
The result is something like this for the default tester case:
%drivers = (
'DBM' => {
driver => 'DBM',
dsn => 'dbi::DBM:t/Test-Database/DBM/DBM',
user => '',
pass => '',
},
'SQLite' => {
driver => 'SQLite',
dsn => 'dbi::SQLite:t/Test-Database/SQLite/SQLite.db',
user => '',
pass => '',
},
);
Power Tester
------------
The result is something like this for tester who was good enough to
decide that they'll run MySQL as well.
$ cat $HOME/.Test-Database
name = mysql
dsn = dbi::mysql:databse=test
user = myuser
pass = mypass
%drivers = (
'DBM' => {
driver => 'DBM',
dsn => 'dbi::DBM:t/Test-Database/DBM/DBM',
user => '',
pass => '',
},
'SQLite' => {
driver => 'SQLite',
dsn => 'dbi::SQLite:t/Test-Database/SQLite/SQLite.db',
user => '',
pass => '',
},
'mysql' => {
driver => 'mysql',
dsn => 'dbi::mysql:databse=test',
user => 'myuser',
pass => 'mypass',
},
);
Module Author
-------------
The result is something like this for module author who runs MySQL
version 4 on port 3307 in addition to the latest MySQL on the default
port:
$ cat $HOME/.Test-Database
name = mysql
dsn = dbi::mysql:databse=test
user = myuser
pass = mypass
name = mysql4
dsn = dbi::mysql:databse=test;port=3307
user = myolduser
pass = myoldpass
%drivers = (
'DBM' => {
driver => 'DBM',
dsn => 'dbi::DBM:t/Test-Database/DBM/DBM',
user => '',
pass => '',
},
'SQLite' => {
driver => 'SQLite',
dsn => 'dbi::SQLite:t/Test-Database/SQLite/SQLite.db',
user => '',
pass => '',
},
'mysql' => {
driver => 'mysql',
dsn => 'dbi::mysql:databse=test',
user => 'myuser',
pass => 'mypass',
},
'mysql4' => {
driver => 'mysql',
dsn => 'dbi::mysql:databse=test;port=3307',
user => 'myolduser',
pass => 'myoldpass',
},
);
Testing
-------
The result of Test::Database->drivers(@list) is clearly then the
intersection of "@list" with "keys %drivers" when @list is defined, and
"keys %drivers" otherwise.
Design Thoughts
---------------
There are two models: file-system based databases and non-file-system
based. If the interface between Test::Database and databases was
strictly limited to DBI/DBD functionality, and we ignore the whole
named database issue, then the entire Driver/* tree could be collapsed
into two, maybe even one, module.
package Test::Database;
# static list of predefined file-based DSNs
%file_drivers = (
DBM => {
file_based => 1,
driver => 'DBM',
dsn => 'dbi:DBM:f_dir=t/Test-Database/DBM',
...
},
SQLite => {
file_based => 1,
driver => 'SQLite',
dsn => 'dbi:SQLite:t/Test-Database/DBM',
...
}
);
# Dynamically generated at 'use'
%drivers = (
%file_drivers + $HOME/.Test-Database definitions,
)
sub drivers {
my @return = ();
my @candidates = map {$drivers{$_}} @_;
foreach my $c (@candidates) {
if (my $driver = Test::Database::Driver->new($c)) {
push(@return, $driver);
}
}
return @return;
}
And the above would work with:
package Test::Database::Driver;
sub new {
my $proto = shift;
my $class = ref($proto) || $proto;
my $self = shift;
# Doing this here saves Test::Database startup time? Maybe not.
if (!eval "require DBD::$self->{driver}; 1;") {
return;
}
if ($self->{file_based}) {
# mkdir t/Test-Database/$self->{driver}
}
# croak now if this fails?
$self->{dbh} = DBI->connect(
$self->{dsn},
$self->{username},
$self->{password}
);
return bless($self,$class);
}
sub cleanup {
my $self = shift;
if ($self->{file_based}) {
$self->{dbh}->disconnect();
# rm -rf t/Test-Database/$self->{driver}
# mkdir t/Test-Database/$self->{driver} exists
$self->{dbh} = DBI->connect(
$self->{dsn},
$self->{username},
$self->{password}
);
}
else {
# DROP TABLE *
# or maybe we do need Driver::<driver>. How do we find the
# list of tables in the database? Does DBI provide such
# functions? Otherwise $self->_cleanup();
}
}
sub connection_info {
my $self = shift;
return ($self->{dsn}, $self->{username}, $self->{password});
}
sub dbh {
my $self = shift;
return $self->{dbh};
}
Which then enables the following, which is more intuitive to me:
foreach my $driver (Test::Database->drivers('SQLite', 'Pg') {
SQL::DB->connect($driver->connection_info);
# or
SQL::DB->set_dbh($driver->dbh);
ok(...., 'test name')
}
Show quoted text> Thank you very much for your input, I appreciate it a lot.
I hope I haven't gone too far with this mail. However regardless of the
outcome, the effort is gladly given, because I will definately
appreciate Test::Database when it's finished!
Cheers,
Mark.
--
Mark Lawrence