Test::More Schema testing.
Mark Addison [Sun, 29 Feb 2004 18:26:53 +0000 (18:26 +0000)]
lib/Test/SQL/Translator.pm [new file with mode: 0644]

diff --git a/lib/Test/SQL/Translator.pm b/lib/Test/SQL/Translator.pm
new file mode 100644 (file)
index 0000000..b2d67e9
--- /dev/null
@@ -0,0 +1,502 @@
+package Test::SQL::Translator;
+
+# ----------------------------------------------------------------------
+# $Id: Translator.pm,v 1.1 2004-02-29 18:26:53 grommit Exp $
+# ----------------------------------------------------------------------
+# Copyright (C) 2003 The SQLFairy Authors
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License as
+# published by the Free Software Foundation; version 2.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
+# 02111-1307  USA
+# -------------------------------------------------------------------
+
+=pod
+
+=head1 NAME
+
+Test::SQL::Translator - Test::More test functions for the Schema objects.
+
+=cut
+
+use strict;
+use warnings;
+
+use base qw(Exporter);
+
+use vars qw($VERSION @EXPORT @EXPORT_OK);
+$VERSION = sprintf "%d.%02d", q$Revision: 1.1 $ =~ /(\d+)\.(\d+)/;
+@EXPORT = qw( 
+    table_ok
+    field_ok
+    constraint_ok
+    index_ok
+    view_ok
+    trigger_ok
+    procedure_ok
+);
+# TODO schema_ok
+
+use Test::More;
+use Test::Exception;
+use SQL::Translator::Schema::Constants;
+
+# $ATTRIBUTES{ <schema_object_name> } = { <attribname> => <default>, ... }
+my %ATTRIBUTES;
+$ATTRIBUTES{field} = {
+    name => undef,
+    data_type => '',
+    default_value => undef,
+    size => '0',
+    is_primary_key => 0,
+    is_unique => 0,
+    is_nullable => 1,
+    is_foreign_key => 0,
+    is_auto_increment => 0,
+    comments => '',
+    extra => {},
+    # foreign_key_reference,
+    is_valid => 1,
+    # order
+};
+$ATTRIBUTES{constraint} = {
+    name => '',
+    type => '',
+    deferrable => 1,
+    expression => '',
+    is_valid => 1,
+    fields => [],
+    match_type => '',
+    options => [],
+    on_delete => '',
+    on_update => '',
+    reference_fields => [],
+    reference_table => '',
+};
+$ATTRIBUTES{'index'} = {
+    fields => [],
+    is_valid => 1,
+    name => "",
+    options => [],
+    type => NORMAL,
+};
+$ATTRIBUTES{'view'} = {
+    name => "",
+    sql => "",
+    fields => [],
+};
+$ATTRIBUTES{'trigger'} = {
+    name                => '',
+    perform_action_when => undef,
+    database_event      => undef,
+    on_table            => undef,
+    action              => undef,
+};
+$ATTRIBUTES{'procedure'} = {
+    name       => '',
+    sql        => '',
+    parameters => [],
+    owner      => '',
+    comments   => '',
+};
+$ATTRIBUTES{table} = {
+    comments   => undef,
+    name       => '',
+    #primary_key => undef, # pkey constraint
+    options    => [],
+    #order      => 0,
+    fields      => undef,
+    constraints => undef,
+    indices     => undef,
+};
+
+
+
+# Given a test hash and schema object name set any attribute keys not present in
+# the test hash to their default value for that schema object type.
+# e.g. default_attribs( $test, "field" );
+sub default_attribs {
+    my ($foo, $what) = @_;
+    die "Can't add default attibs - unkown Scheam object type '$what'."
+    unless exists $ATTRIBUTES{$what};
+    $foo->{$_} = $ATTRIBUTES{$what}{$_}
+    foreach grep !exists($foo->{$_}), keys %{$ATTRIBUTES{$what}};
+    return $foo;
+}
+
+# Format test name so it will prepend the test names used below.
+sub t_name {
+    my $name = shift;
+    $name ||= "";
+    $name = "$name - " if $name;
+    return $name;
+}
+
+sub field_ok {
+    my ($f1,$test,$name) = @_;
+    my $t_name = t_name($name);
+    default_attribs($test,"field");
+
+    unless ($f1) {
+        fail " Field '$test->{name}' doesn't exist!";
+        return;
+    }
+
+    is( $f1->name, $test->{name}, "${t_name}Field name '$test->{name}'" );
+
+    is( $f1->is_valid, $test->{is_valid},
+    "$t_name    is".($test->{is_valid} ? '' : 'not ').'valid' );
+
+    is( $f1->data_type, $test->{data_type},
+        "$t_name    type is '$test->{data_type}'" );
+
+    is( $f1->size, $test->{size}, "$t_name    size is '$test->{size}'" );
+
+    is( $f1->default_value, $test->{default_value},
+    "$t_name    default value is "
+    .(defined($test->{default_value}) ? "'$test->{default_value}'" : "UNDEF" )
+    );
+
+    is( $f1->is_nullable, $test->{is_nullable},
+    "$t_name    ".($test->{is_nullable} ? 'can' : 'cannot').' be null' );
+
+    is( $f1->is_unique, $test->{is_unique},
+    "$t_name    ".($test->{is_unique} ? 'can' : 'cannot').' be unique' );
+
+    is( $f1->is_primary_key, $test->{is_primary_key},
+    "$t_name    is ".($test->{is_primary_key} ? '' : 'not ').'a primary_key' );
+
+    is( $f1->is_foreign_key, $test->{is_foreign_key},
+    "$t_name    is ".($test->{is_foreign_key} ? '' : 'not').' a foreign_key' );
+
+    is( $f1->is_auto_increment, $test->{is_auto_increment},
+    "$t_name    is "
+    .($test->{is_auto_increment} ?  '' : 'not ').'an auto_increment' );
+
+    is( $f1->comments, $test->{comments}, "$t_name    comments" );
+
+    is_deeply( { $f1->extra }, $test->{extra}, "$t_name    extra" );
+}
+
+sub constraint_ok {
+    my ($obj,$test,$name) = @_;
+    my $t_name = t_name($name);
+    default_attribs($test,"constraint");
+
+    is( $obj->name, $test->{name}, "${t_name}Constraint '$test->{name}'" );
+
+    is( $obj->type, $test->{type}, "$t_name    type is '$test->{type}'" );
+
+    is( $obj->deferrable, $test->{deferrable},
+    "$t_name    ".($test->{deferrable} ? 'can' : 'cannot').' be deferred' );
+
+    is( $obj->is_valid, $test->{is_valid},
+    "$t_name    is ".($test->{is_valid} ? '' : 'not ').'valid' );
+
+    is($obj->table->name,$test->{table},"$t_name    table is '$test->{table}'" );
+
+    is( $obj->expression, $test->{expression},
+    "$t_name    expression is '$test->{expression}'" );
+
+    is_deeply( [$obj->fields], $test->{fields},
+    "$t_name    fields are '".join(",",@{$test->{fields}})."'" );
+
+    is( $obj->reference_table, $test->{reference_table},
+    "$t_name    reference_table is '$test->{reference_table}'" );
+
+    is_deeply( [$obj->reference_fields], $test->{reference_fields},
+    "$t_name    reference_fields are '".join(",",@{$test->{reference_fields}})."'" );
+
+    is( $obj->match_type, $test->{match_type},
+    "$t_name    match_type is '$test->{match_type}'" );
+
+    is( $obj->on_delete, $test->{on_delete},
+    "$t_name    on_delete is '$test->{on_delete}'" );
+
+    is( $obj->on_update, $test->{on_update},
+    "$t_name    on_update is '$test->{on_update}'" );
+
+    is_deeply( [$obj->options], $test->{options},
+    "$t_name    options are '".join(",",@{$test->{options}})."'" );
+}
+
+sub index_ok {
+    my ($obj,$test,$name) = @_;
+    my $t_name = t_name($name);
+    default_attribs($test,"index");
+
+    is( $obj->name, $test->{name}, "${t_name}Index '$test->{name}'" );
+
+    is( $obj->is_valid, $test->{is_valid},
+    "$t_name    is ".($test->{is_valid} ? '' : 'not ').'valid' );
+
+    is( $obj->type, $test->{type}, "$t_name    type is '$test->{type}'" );
+
+    is_deeply( [$obj->fields], $test->{fields},
+    "$t_name    fields are '".join(",",@{$test->{fields}})."'" );
+
+    is_deeply( [$obj->options], $test->{options},
+    "$t_name    options are '".join(",",@{$test->{options}})."'" );
+}
+
+sub trigger_ok {
+    my ($obj,$test,$name) = @_;
+    my $t_name = t_name($name);
+    default_attribs($test,"index");
+
+    is( $obj->name, $test->{name}, "${t_name}Constraint '$test->{name}'" );
+
+    is( $obj->is_valid, $test->{is_valid},
+        "$t_name    is ".($test->{is_valid} ? '' : 'not ').'valid' );
+
+    is( $obj->perform_action_when, $test->{perform_action_when},
+        "$t_name    perform_action_when is '$test->{perform_action_when}'" );
+
+    is( $obj->database_event, $test->{database_event},
+        "$t_name    database_event is '$test->{database_event}'" );
+
+    is( $obj->on_table, $test->{on_table},
+        "$t_name    on_table is '$test->{on_table}'" );
+
+    is( $obj->action, $test->{action}, "$t_name    action is '$test->{action}'" );
+}
+
+sub view_ok {
+    my ($obj,$test,$name) = @_;
+    my $t_name = t_name($name);
+    default_attribs($test,"index");
+
+    #isa_ok( $v, 'SQL::Translator::Schema::View', 'View' );
+
+    is( $obj->name, $test->{name}, "${t_name}View '$test->{name}'" );
+
+    is( $obj->is_valid, $test->{is_valid},
+    "$t_name    is ".($test->{is_valid} ? '' : 'not ').'valid' );
+
+    is( $obj->sql, $test->{sql}, "$t_name    sql is '$test->{sql}'" );
+
+    is_deeply( [$obj->fields], $test->{fields},
+    "$t_name    fields are '".join(",",@{$test->{fields}})."'" );
+}
+
+sub procedure_ok {
+    my ($obj,$test,$name) = @_;
+    my $t_name = t_name($name);
+    default_attribs($test,"index");
+
+    #isa_ok( $v, 'SQL::Translator::Schema::View', 'View' );
+
+    is( $obj->name, $test->{name}, "${t_name}Procedure '$test->{name}'" );
+
+    is( $obj->sql, $test->{sql}, "$t_name    sql is '$test->{sql}'" );
+
+    is_deeply( [$obj->parameters], $test->{parameters},
+    "$t_name    parameters are '".join(",",@{$test->{parameters}})."'" );
+
+    is( $obj->comments, $test->{comments}, 
+        "$t_name    comments is '$test->{comments}'" );
+
+    is( $obj->owner, $test->{owner}, "$t_name    owner is '$test->{owner}'" );
+}
+
+sub table_ok {
+    my ($obj,$test,$name) = @_;
+    my $t_name = t_name($name);
+    default_attribs($test,"table");
+    my %arg = %$test;
+
+    my $tbl_name = $arg{name} || die "Need a table name to test.";
+    is( $obj->{name}, $arg{name}, "${t_name}Table name '$arg{name}'" );
+
+    is_deeply( [$obj->options], $test->{options},
+    "$t_name    options are '".join(",",@{$test->{options}})."'" );
+
+    # Fields
+    if ( $arg{fields} ) {
+        my @fldnames = map { $_->{name} } @{$arg{fields}};
+        is_deeply( [ map {$_->name}   $obj->get_fields ],
+                   [ map {$_->{name}} @{$arg{fields}} ],
+                   "${t_name}Table $tbl_name fields match" );
+        foreach ( @{$arg{fields}} ) {
+            my $f_name = $_->{name} || die "Need a field name to test.";
+            field_ok( $obj->get_field($f_name), $_, $name );
+        }
+    }
+    else {
+        is(scalar($obj->get_fields), undef,
+            "${t_name}Table $tbl_name has no fields.");
+    }
+
+    # Constraints and indices
+    my %bits = (
+        constraint => "constraints",
+        'index'    => "indices",
+    );
+    while ( my($foo,$plural) = each %bits ) {
+        next unless defined $arg{$plural};
+        if ( my @tfoo = @{$arg{$plural}} ) {
+            my $meth = "get_$plural";
+            my @foo = $obj->$meth;
+            is(scalar(@foo), scalar(@tfoo),
+            "${t_name}Table $tbl_name has ".scalar(@tfoo)." $plural");
+            foreach ( @foo ) {
+                my $ans = { table => $obj->name, %{shift @tfoo}};
+                my $meth = "${foo}_ok";
+                { no strict 'refs';
+                    $meth->( $_, $ans, $name  );
+                }
+            }
+        }
+    }
+}
+
+sub schema_ok {
+    my ($obj,$test,$name) = @_;
+    my $t_name = t_name($name);
+    default_attribs($test,"schema");
+}
+
+1; # compile please ===========================================================
+__END__
+
+=pod
+
+=head1 SYNOPSIS
+
+ # t/magic.t
+
+ use FindBin '$Bin';
+ use Test::More;
+ use Test::SQL::Translator;
+
+ # Run parse
+ my $sqlt = SQL::Translator->new(
+     parser => "Magic",
+     filename => "$Bin/data/magic/test.magic",
+     ... 
+ );
+ ...
+ my $schema = $sqlt->schema;
+ # Test the table it produced.
+ table_ok( $schema->get_table("Customer"), {
+     name => "Customer",
+     fields => [
+         {
+             name => "CustomerID",
+             data_type => "INT",
+             size => 12,
+             default_value => undef,
+             is_nullable => 0,
+             is_primary_key => 1,
+         },
+         {
+             name => "bar",
+             data_type => "VARCHAR",
+             size => 255,
+             is_nullable => 0,
+         },
+     ],
+     constraints => [
+         {
+             type => "PRIMARY KEY",
+             fields => "CustomerID",
+         },
+     ],
+     indices => [
+         {
+             name => "barindex",
+             fields => ["bar"],
+         },
+     ],
+ });
+
+=head1 DESCSIPTION
+
+Provides a set of Test::More tests for Schema objects. Tesing a parsed
+schema is then as easy as writing a perl data structure describing how you
+expect the schema to look.
+
+The data structures given to the test subs don't have to include all the 
+possible values, only the ones you expect to have changed. Any left out will be
+tested to make sure they are still at their default value. This is a usefull
+check that you your parser hasn't accidentally set schema values you didn't
+expect it to. (And makes tests look nice and long ;-)
+
+For an example of the output run the t/16xml-parser.t test.
+
+=head1 Tests
+
+All the tests take a first arg of the schema object to test, followed by a 
+hash ref describing how you expect that object to look (you only need give the
+attributes you expect to have changed from the default).
+The 3rd arg is an optional test name to pre-pend to all the generated test 
+names.
+
+=head2 table_ok
+
+=head2 field_ok
+
+=head2 constraint_ok
+
+=head2 index_ok
+
+=head2 view_ok
+
+=head2 trigger_ok
+
+=head2 procedure_ok
+
+=head1 EXPORTS
+
+table_ok, field_ok, constraint_ok, index_ok, view_ok, trigger_ok, procedure_ok
+
+=head1 TODO
+
+=over 4
+
+=item Test the tests!
+
+=item schema_ok()
+
+Test whole schema.
+
+=item Test skipping
+
+As the test subs wrap up lots of tests in one call you can't skip idividual
+tests only whole sets e.g. a whole table or field.
+We could add skip_* items to the test hashes to allow per test skips. e.g.
+
+ skip_is_primary_key => "Need to fix primary key parsing.",
+
+=item yaml test specs
+
+Maybe have the test subs also accept yaml for the test hash ref as its a much
+nicer for writing big data structures. We can then define tests as in input
+schema file and test yaml file to compare it against.
+
+=back
+
+=head1 BUGS
+
+=head1 AUTHOR
+
+Mark D. Addison E<lt>mark.addison@itn.co.ukE<gt>.
+
+Thanks to Ken Y. Clark for the original table and field test code taken from
+his mysql test.
+
+=head1 SEE ALSO
+
+perl(1), SQL::Translator, SQL::Translator::Schema, Test::More.
+
+=cut