From 0bac7826012b3536bfa62bf2fc1adee91da55b26 Mon Sep 17 00:00:00 2001 From: Tomas Cohen Arazi Date: Wed, 3 Jun 2015 15:54:57 -0300 Subject: [PATCH] Bug 14256: Check for unique constraint to regenerate random data Unique constraints should be checked when creating random data. Otherwise we get failures when the generated data already exists on the DB. This patch takes advantage of ->unique_constraints() to do the job, looping through all the unique constraints defined for the source. Signed-off-by: Tomas Cohen Arazi Signed-off-by: Kyle M Hall Signed-off-by: Mason James --- t/lib/TestBuilder.pm | 456 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 456 insertions(+) create mode 100644 t/lib/TestBuilder.pm diff --git a/t/lib/TestBuilder.pm b/t/lib/TestBuilder.pm new file mode 100644 index 0000000000..7814df380f --- /dev/null +++ b/t/lib/TestBuilder.pm @@ -0,0 +1,456 @@ +package t::lib::TestBuilder; + +use Modern::Perl; +use Koha::Database; +use String::Random; + + +my $gen_type = { + tinyint => \&_gen_int, + smallint => \&_gen_int, + mediumint => \&_gen_int, + integer => \&_gen_int, + bigint => \&_gen_int, + + float => \&_gen_real, + decimal => \&_gen_real, + double_precision => \&_gen_real, + + timestamp => \&_gen_date, + datetime => \&_gen_date, + date => \&_gen_date, + + char => \&_gen_text, + varchar => \&_gen_text, + tinytext => \&_gen_text, + text => \&_gen_text, + mediumtext => \&_gen_text, + longtext => \&_gen_text, + + set => \&_gen_set_enum, + enum => \&_gen_set_enum, + + tinyblob => \&_gen_blob, + mediumblob => \&_gen_blob, + blob => \&_gen_blob, + longblob => \&_gen_blob, +}; + +our $default_value = { + UserPermission => { + borrowernumber => { + surname => 'my surname', + address => 'my adress', + city => 'my city', + branchcode => { + branchcode => 'cB', + branchname => 'my branchname', + }, + categorycode => { + categorycode => 'cC', + hidelostitems => 0, + category_type => 'A', + default_privacy => 'default', + }, + privacy => 1, + }, + module_bit => { + module_bit => { + bit => '10', + }, + code => 'my code', + }, + code => undef, + }, +}; +$default_value->{UserPermission}->{code} = $default_value->{UserPermission}->{module_bit}; + + +sub new { + my ($class) = @_; + my $self = {}; + bless( $self, $class ); + + $self->schema( Koha::Database->new()->schema ); + $self->schema->txn_begin(); + $self->schema->storage->sql_maker->quote_char('`'); + return $self; +} + +sub schema { + my ($self, $schema) = @_; + + if( defined( $schema ) ) { + $self->{schema} = $schema; + } + return $self->{schema}; +} + +sub clear { + my ($self, $params) = @_; + my $source = $self->schema->resultset( $params->{source} ); + return $source->delete_all(); +} + +sub build { + my ($self, $params) = @_; + my $source = $params->{source} || return; + my $value = $params->{value}; + my $only_fk = $params->{only_fk} || 0; + + my $col_values = $self->_buildColumnValues({ + source => $source, + value => $value, + }); + + my $data; + my $foreign_keys = $self->_getForeignKeys( { source => $source } ); + for my $fk ( @$foreign_keys ) { + my $fk_value; + my $col_name = $fk->{keys}->[0]->{col_name}; + if( ref( $col_values->{$col_name} ) eq 'HASH' ) { + $fk_value = $col_values->{$col_name}; + } + elsif( defined( $col_values->{$col_name} ) ) { + next; + } + + my $fk_row = $self->build({ + source => $fk->{source}, + value => $fk_value, + }); + + my $keys = $fk->{keys}; + for my $key( @$keys ) { + $col_values->{ $key->{col_name} } = $fk_row->{ $key->{col_fk_name} }; + $data->{ $key->{col_name} } = $fk_row; + } + } + + my $new_row; + if( $only_fk ) { + $new_row = $col_values; + } + else { + $new_row = $self->_storeColumnValues({ + source => $source, + values => $col_values, + }); + } + $new_row->{_fk} = $data if( defined( $data ) ); + return $new_row; +} + +sub _formatSource { + my ($params) = @_; + my $source = $params->{source}; + $source =~ s|(\w+)$|$1|; + return $source; +} + +sub _buildColumnValues { + my ($self, $params) = @_; + my $source = _formatSource( { source => $params->{source} } ); + my $value = $params->{value}; + + my $col_values; + my @columns = $self->schema->source($source)->columns; + my %unique_constraints = $self->schema->source($source)->unique_constraints(); + + my $values_ok = 0; + my $at_least_one_constraint_failed; + + while ( not $values_ok ) { + + $at_least_one_constraint_failed = 0; + # generate random values + for my $col_name( @columns ) { + my $col_value = $self->_buildColumnValue({ + source => $source, + column_name => $col_name, + value => $value, + }); + $col_values->{$col_name} = $col_value if( defined( $col_value ) ); + } + + # If default values are set, maybe the data exist in the DB + # But no need to wait for another value + last if exists( $default_value->{$source} ); + + if ( scalar keys %unique_constraints > 0 ) { + + # verify the data would respect each unique constraint + foreach my $constraint (keys %unique_constraints) { + + my $condition; + my @constraint_columns = $unique_constraints{$constraint}; + # loop through all constraint columns and build the condition + foreach my $constraint_column ( @constraint_columns ) { + # build the filter + $condition->{ $constraint_column } = + $col_values->{ $constraint_column }; + } + + my $count = $self->schema + ->resultset( $source ) + ->search( $condition ) + ->count(); + if ( $count > 0 ) { + $at_least_one_constraint_failed = 1; + # no point checking more stuff, exit the loop + last; + } + } + + if ( $at_least_one_constraint_failed ) { + $values_ok = 0; + } else { + $values_ok = 1; + } + + } else { + $values_ok = 1; + } + } + return $col_values; +} + +# Returns [ { +# rel_name => $rel_name, +# source => $table_name, +# keys => [ { +# col_name => $col_name, +# col_fk_name => $col_fk_name, +# }, ... ] +# }, ... ] +sub _getForeignKeys { + my ($self, $params) = @_; + my $source = $self->schema->source( $params->{source} ); + + my @foreign_keys = (); + my @relationships = $source->relationships; + for my $rel_name( @relationships ) { + my $rel_info = $source->relationship_info($rel_name); + if( $rel_info->{attrs}->{is_foreign_key_constraint} ) { + my $rel = { + rel_name => $rel_name, + source => $rel_info->{source}, + }; + + my @keys = (); + while( my ($col_fk_name, $col_name) = each(%{$rel_info->{cond}}) ) { + $col_name =~ s|self.(\w+)|$1|; + $col_fk_name =~ s|foreign.(\w+)|$1|; + push @keys, { + col_name => $col_name, + col_fk_name => $col_fk_name, + }; + } + $rel->{keys} = \@keys; + + push @foreign_keys, $rel; + } + } + return \@foreign_keys; +} + +sub _storeColumnValues { + my ($self, $params) = @_; + my $source = $params->{source}; + my $col_values = $params->{values}; + + my $new_row; + eval { + $new_row = $self->schema->resultset($source)->update_or_create($col_values); + }; + die "$source - $@\n" if ($@); + + eval { + $new_row = { $new_row->get_columns }; + }; + warn "$source - $@\n" if ($@); + return $new_row; +} + +sub _buildColumnValue { + my ($self, $params) = @_; + my $source = $params->{source}; + my $value = $params->{value}; + my $col_name = $params->{column_name}; + my $col_info = $self->schema->source($source)->column_info($col_name); + + my $col_value; + if( exists( $value->{$col_name} ) ) { + $col_value = $value->{$col_name}; + } + elsif( exists( $default_value->{$source}->{$col_name} ) ) { + $col_value = $default_value->{$source}->{$col_name}; + } + elsif( not $col_info->{default_value} and not $col_info->{is_auto_increment} and not $col_info->{is_foreign_key} ) { + eval { + my $data_type = $col_info->{data_type}; + $data_type =~ s| |_|; + $col_value = $gen_type->{$data_type}->( $self, { info => $col_info } ); + }; + die "The type $col_info->{data_type} is not defined\n" if ($@); + } + return $col_value; +} + + +sub _gen_int { + my ($self, $params) = @_; + my $data_type = $params->{info}->{data_type}; + + my $max = 1; + if( $data_type eq 'tinyint' ) { + $max = 127; + } + elsif( $data_type eq 'smallint' ) { + $max = 32767; + } + elsif( $data_type eq 'mediumint' ) { + $max = 8388607; + } + elsif( $data_type eq 'integer' ) { + $max = 2147483647; + } + elsif( $data_type eq 'bigint' ) { + $max = 9223372036854775807; + } + return int( rand($max+1) ); +} + +sub _gen_real { + my ($self, $params) = @_; + my $max = 10 ** 38; + if( defined( $params->{info}->{size} ) ) { + $max = 10 ** ($params->{info}->{size}->[0] - $params->{info}->{size}->[1]); + } + return rand($max) + 1; +} + +sub _gen_date { + my ($self, $params) = @_; + return $self->schema->storage->datetime_parser->format_datetime(DateTime->now()); +} + +sub _gen_text { + my ($self, $params) = @_; + # From perldoc String::Random + # max: specify the maximum number of characters to return for * and other + # regular expression patters that don't return a fixed number of characters + my $regex = '[A-Za-z][A-Za-z0-9_]*'; + my $size = $params->{info}{size}; + if ( defined $size and $size > 1 ) { + $size--; + } elsif ( defined $size and $size == 1 ) { + $regex = '[A-Za-z]'; + } + my $random = String::Random->new( max => $size ); + return $random->randregex($regex); +} + +sub _gen_set_enum { + my ($self, $params) = @_; + return $params->{info}->{extra}->{list}->[0]; +} + +sub _gen_blob { + my ($self, $params) = @_;; + return 'b'; +} + + +sub DESTROY { + my $self = shift; + $self->schema->txn_rollback(); +} + + +=head1 NAME + +t::lib::TestBuilder.pm - Koha module to simplify the writing of tests + +=head1 SYNOPSIS + + use t::lib::TestBuilder; + +Koha module to insert the foreign keys automatically for the tests + +=head1 DESCRIPTION + +This module allows to insert automatically an entry in the database. All the database changes are wrapped in a transaction. +The foreign keys are created according to the DBIx::Class schema. +The taken values are the values by default if it is possible or randomly generated. + +=head1 FUNCTIONS + +=head2 new + + $builder = t::lib::TestBuilder->new() + +Constructor - Begins a transaction and returns the object TestBuilder + +=head2 schema + + $schema = $builder->schema + +Getter - Returns the schema of DBIx::Class + +=head2 clear + + $builder->clear({ source => $source_name }) + +=over + +=item C<$source_name> is the name of the source in the DBIx::Class schema (required) + +=back + +Clears all the data of this source (database table) + +=head2 build + + $builder->build({ + source => $source_name, + value => $value, + only_fk => $only_fk, + }) + +=over + +=item C<$source_name> is the name of the source in the DBIx::Class schema (required) + +=item C<$value> is the values for the entry (optional) + +=item C<$only_fk> is a boolean to indicate if only the foreign keys are created (optional) + +=back + +Inserts an entry in the database by instanciating all the foreign keys. +The values can be specified, the values which are not given are default values if they exists or generated randomly. +Returns the values of the entry as a hashref with an extra key : _fk which contains all the values of the generated foreign keys. + +=head1 AUTHOR + +Yohann Dufour + +=head1 COPYRIGHT + +Copyright 2014 - Biblibre SARL + +=head1 LICENSE + +This file is part of Koha. + +Koha 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; either version 3 of the License, or (at your option) any later version. + +Koha 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 Koha; if not, see . + +=cut + +1; -- 2.39.5