1 package t::lib::TestBuilder;
10 bless( $self, $class );
12 $self->schema( Koha::Database->new()->schema );
13 $self->schema->storage->sql_maker->quote_char('`');
15 $self->{gen_type} = _gen_type();
16 $self->{default_values} = _gen_default_values();
21 my ($self, $schema) = @_;
23 if( defined( $schema ) ) {
24 $self->{schema} = $schema;
26 return $self->{schema};
29 # sub clear has been obsoleted; use delete_all from the schema resultset
32 my ( $self, $params ) = @_;
33 my $source = $params->{source} || return;
34 my @recs = ref( $params->{records} ) eq 'ARRAY'?
35 @{$params->{records}}: ( $params->{records} // () );
36 # tables without PK are not supported
37 my @pk = $self->schema->source( $source )->primary_columns;
40 foreach my $rec ( @recs ) {
41 # delete only works when you supply full primary key values
42 # $cond does not include searches for undef (not allowed in PK)
43 my $cond = { map { defined $rec->{$_}? ($_, $rec->{$_}): (); } @pk };
44 next if keys %$cond < @pk;
45 $self->schema->resultset( $source )->search( $cond )->delete;
46 # we clear the pk columns in the supplied hash
47 # this indirectly signals at least an attempt to delete
48 map { delete $rec->{$_}; } @pk;
55 # build returns a hash of column values for a created record, or undef
56 # build does NOT update a record, or pass back values of an existing record
57 my ($self, $params) = @_;
58 my $source = $params->{source} || return;
59 my $value = $params->{value};
61 my $col_values = $self->_buildColumnValues({
65 return if !$col_values; # did not meet unique constraints?
67 # loop thru all fk and create linked records if needed
68 # fills remaining entries in $col_values
69 my $foreign_keys = $self->_getForeignKeys( { source => $source } );
70 for my $fk ( @$foreign_keys ) {
71 # skip when FK points to itself: e.g. borrowers:guarantorid
72 next if $fk->{source} eq $source;
73 my $keys = $fk->{keys};
74 my $tbl = $fk->{source};
75 my $res = $self->_create_links( $tbl, $keys, $col_values, $value );
76 return if !$res; # failed: no need to go further
77 foreach( keys %$res ) { # save new values
78 $col_values->{$_} = $res->{$_};
82 # store this record and return hashref
83 return $self->_storeColumnValues({
85 values => $col_values,
89 # ------------------------------------------------------------------------------
90 # Internal helper routines
93 # returns undef for failure to create linked records
94 # otherwise returns hashref containing new column values for parent record
95 my ( $self, $linked_tbl, $keys, $col_values, $value ) = @_;
98 my ( $cnt_scalar, $cnt_null ) = ( 0, 0 );
100 # First, collect all values for creating a linked record (if needed)
101 foreach my $fk ( @$keys ) {
102 my ( $col, $destcol ) = ( $fk->{col_name}, $fk->{col_fk_name} );
103 if( ref( $value->{$col} ) eq 'HASH' ) {
104 # add all keys from the FK hash
105 $fk_value = { %{ $value->{$col} }, %$fk_value };
107 if( exists $col_values->{$col} ) {
108 # add specific value (this does not necessarily exclude some
109 # values from the hash in the preceding if)
110 $fk_value->{ $destcol } = $col_values->{ $col };
112 $cnt_null++ if !defined( $col_values->{$col} );
116 # If we saw all FK columns, first run the following checks
117 if( $cnt_scalar == @$keys ) {
118 # if one or more fk cols are null, the FK constraint will not be forced
119 return {} if $cnt_null > 0;
120 # does the record exist already?
121 return {} if $self->schema->resultset($linked_tbl)->find( $fk_value );
123 # create record with a recursive build call
124 my $row = $self->build({ source => $linked_tbl, value => $fk_value });
125 return if !$row; # failure
127 # Finally, only return the new values
129 foreach my $fk ( @$keys ) {
130 my ( $col, $destcol ) = ( $fk->{col_name}, $fk->{col_fk_name} );
131 next if exists $col_values->{ $col };
132 $rv->{ $col } = $row->{ $destcol };
134 return $rv; # success
139 my $source = $params->{source} || return;
140 $source =~ s|(\w+)$|$1|;
144 sub _buildColumnValues {
145 my ($self, $params) = @_;
146 my $source = _formatSource( $params ) || return;
147 my $original_value = $params->{value};
150 my @columns = $self->schema->source($source)->columns;
151 my %unique_constraints = $self->schema->source($source)->unique_constraints();
154 # we try max three times if there are unique constraints
155 BUILD_VALUE: while ( $build_value ) {
156 # generate random values for all columns
157 for my $col_name( @columns ) {
158 my $valref = $self->_buildColumnValue({
160 column_name => $col_name,
161 value => $original_value,
163 return if !$valref; # failure
164 if( @$valref ) { # could be empty
165 # there will be only one value, but it could be undef
166 $col_values->{$col_name} = $valref->[0];
170 # verify the data would respect each unique constraint
171 # note that this is INCOMPLETE since not all col_values are filled
172 CONSTRAINTS: foreach my $constraint (keys %unique_constraints) {
175 my $constraint_columns = $unique_constraints{$constraint};
176 # loop through all constraint columns and build the condition
177 foreach my $constraint_column ( @$constraint_columns ) {
179 # if one column does not exist or is undef, skip it
180 # an insert with a null will not trigger the constraint
182 if !exists $col_values->{ $constraint_column } ||
183 !defined $col_values->{ $constraint_column };
184 $condition->{ $constraint_column } =
185 $col_values->{ $constraint_column };
187 my $count = $self->schema
188 ->resultset( $source )
189 ->search( $condition )
192 # no point checking more stuff, exit the loop
197 last; # you passed all tests
199 return $col_values if $build_value > 0;
201 # if you get here, we have a problem
202 warn "Violation of unique constraint in $source";
206 sub _getForeignKeys {
208 # Returns the following arrayref
209 # [ [ source => name, keys => [ col_name => A, col_fk_name => B ] ], ... ]
210 # The array gives source name and keys for each FK constraint
212 my ($self, $params) = @_;
213 my $source = $self->schema->source( $params->{source} );
215 my ( @foreign_keys, $check_dupl );
216 my @relationships = $source->relationships;
217 for my $rel_name( @relationships ) {
218 my $rel_info = $source->relationship_info($rel_name);
219 if( $rel_info->{attrs}->{is_foreign_key_constraint} ) {
220 $rel_info->{source} =~ s/^.*:://g;
221 my $rel = { source => $rel_info->{source} };
224 while( my ($col_fk_name, $col_name) = each(%{$rel_info->{cond}}) ) {
225 $col_name =~ s|self.(\w+)|$1|;
226 $col_fk_name =~ s|foreign.(\w+)|$1|;
228 col_name => $col_name,
229 col_fk_name => $col_fk_name,
232 # check if the combination table and keys is unique
233 # so skip double belongs_to relations (as in Biblioitem)
234 my $tag = $rel->{source}. ':'.
235 join ',', sort map { $_->{col_name} } @keys;
236 next if $check_dupl->{$tag};
237 $check_dupl->{$tag} = 1;
238 $rel->{keys} = \@keys;
239 push @foreign_keys, $rel;
242 return \@foreign_keys;
245 sub _storeColumnValues {
246 my ($self, $params) = @_;
247 my $source = $params->{source};
248 my $col_values = $params->{values};
249 my $new_row = $self->schema->resultset( $source )->create( $col_values );
250 return $new_row? { $new_row->get_columns }: {};
253 sub _buildColumnValue {
254 # returns an arrayref if all goes well
255 # an empty arrayref typically means: auto_incr column or fk column
256 # undef means failure
257 my ($self, $params) = @_;
258 my $source = $params->{source};
259 my $value = $params->{value};
260 my $col_name = $params->{column_name};
262 my $col_info = $self->schema->source($source)->column_info($col_name);
265 if( $col_info->{is_auto_increment} ) {
266 if( exists $value->{$col_name} ) {
267 warn "Value not allowed for auto_incr $col_name in $source";
270 # otherwise: no need to assign a value
271 } elsif( $col_info->{is_foreign_key} || _should_be_fk($source,$col_name) ) {
272 if( exists $value->{$col_name} ) {
273 if( !defined $value->{$col_name} && !$col_info->{is_nullable} ) {
274 # This explicit undef is not allowed
275 warn "Null value for $col_name in $source not allowed";
278 if( ref( $value->{$col_name} ) ne 'HASH' ) {
279 push @$retvalue, $value->{$col_name};
281 # sub build will handle a passed hash value later on
283 } elsif( ref( $value->{$col_name} ) eq 'HASH' ) {
284 # this is not allowed for a column that is not a FK
285 warn "Hash not allowed for $col_name in $source";
287 } elsif( exists $value->{$col_name} ) {
288 if( !defined $value->{$col_name} && !$col_info->{is_nullable} ) {
289 # This explicit undef is not allowed
290 warn "Null value for $col_name in $source not allowed";
293 push @$retvalue, $value->{$col_name};
294 } elsif( exists $self->{default_values}{$source}{$col_name} ) {
295 push @$retvalue, $self->{default_values}{$source}{$col_name};
297 my $data_type = $col_info->{data_type};
298 $data_type =~ s| |_|;
299 if( my $hdlr = $self->{gen_type}->{$data_type} ) {
300 push @$retvalue, &$hdlr( $self, { info => $col_info } );
302 warn "Unknown type $data_type for $col_name in $source";
310 # This sub is only needed for inconsistencies in the schema
311 # A column is not marked as FK, but a belongs_to relation is defined
312 my ( $source, $column ) = @_;
313 my $inconsistencies = {
314 'Item.biblionumber' => 1, #FIXME: Please remove me when I become FK
316 return $inconsistencies->{ "$source.$column" };
321 tinyint => \&_gen_int,
322 smallint => \&_gen_int,
323 mediumint => \&_gen_int,
324 integer => \&_gen_int,
325 bigint => \&_gen_int,
327 float => \&_gen_real,
328 decimal => \&_gen_real,
329 double_precision => \&_gen_real,
331 timestamp => \&_gen_datetime,
332 datetime => \&_gen_datetime,
336 varchar => \&_gen_text,
337 tinytext => \&_gen_text,
339 mediumtext => \&_gen_text,
340 longtext => \&_gen_text,
342 set => \&_gen_set_enum,
343 enum => \&_gen_set_enum,
345 tinyblob => \&_gen_blob,
346 mediumblob => \&_gen_blob,
348 longblob => \&_gen_blob,
353 my ($self, $params) = @_;
354 my $data_type = $params->{info}->{data_type};
357 if( $data_type eq 'tinyint' ) {
360 elsif( $data_type eq 'smallint' ) {
363 elsif( $data_type eq 'mediumint' ) {
366 elsif( $data_type eq 'integer' ) {
369 elsif( $data_type eq 'bigint' ) {
370 $max = 9223372036854775807;
372 return int( rand($max+1) );
376 my ($self, $params) = @_;
378 if( defined( $params->{info}->{size} ) ) {
379 $max = 10 ** ($params->{info}->{size}->[0] - $params->{info}->{size}->[1]);
381 return rand($max) + 1;
385 my ($self, $params) = @_;
386 return $self->schema->storage->datetime_parser->format_date(DateTime->now())
390 my ($self, $params) = @_;
391 return $self->schema->storage->datetime_parser->format_datetime(DateTime->now());
395 my ($self, $params) = @_;
396 # From perldoc String::Random
397 # max: specify the maximum number of characters to return for * and other
398 # regular expression patters that don't return a fixed number of characters
399 my $regex = '[A-Za-z][A-Za-z0-9_]*';
400 my $size = $params->{info}{size};
401 if ( defined $size and $size > 1 ) {
403 } elsif ( defined $size and $size == 1 ) {
406 my $random = String::Random->new( max => $size );
407 return $random->randregex($regex);
411 my ($self, $params) = @_;
412 return $params->{info}->{extra}->{list}->[0];
416 my ($self, $params) = @_;;
420 sub _gen_default_values {
424 more_subfields_xml => undef,
431 t::lib::TestBuilder.pm - Koha module to create test records
435 use t::lib::TestBuilder;
436 my $builder = t::lib::TestBuilder->new;
438 # The following call creates a patron, linked to branch CPL.
439 # Surname is provided, other columns are randomly generated.
440 # Branch CPL is created if it does not exist.
441 my $patron = $builder->build({
442 source => 'Borrower',
443 value => { surname => 'Jansen', branchcode => 'CPL' },
448 This module automatically creates database records for you.
449 If needed, records for foreign keys are created too.
450 Values will be randomly generated if not passed to TestBuilder.
451 Note that you should wrap these actions in a transaction yourself.
457 my $builder = t::lib::TestBuilder->new;
459 Constructor - Returns the object TestBuilder
463 my $schema = $builder->schema;
465 Getter - Returns the schema of DBIx::Class
471 records => $patron, # OR: records => [ $patron, ... ],
474 Delete individual records, created by builder.
475 Returns the number of delete attempts, or undef.
479 $builder->build({ source => $source_name, value => $value });
481 Create a test record in the table, represented by $source_name.
482 The name is required and must conform to the DBIx::Class schema.
483 Values may be specified by the optional $value hashref. Will be
484 randomized otherwise.
485 If needed, TestBuilder creates linked records for foreign keys.
486 Returns the values of the new record as a hashref, or undef if
487 the record could not be created.
489 Note that build also supports recursive hash references inside the
490 value hash for foreign key columns, like:
492 column1 => 'some_value',
494 columnA => 'another_value',
497 The hash for fk_col2 here means: create a linked record with build
498 where columnA has this value. In case of a composite FK the hashes
501 Realize that passing primary key values to build may result in undef
502 if a record with that primary key already exists.
506 Yohann Dufour <yohann.dufour@biblibre.com>
508 Koha Development Team
512 Copyright 2014 - Biblibre SARL
516 This file is part of Koha.
518 Koha is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
519 the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
521 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.
523 You should have received a copy of the GNU General Public License along with Koha; if not, see <http://www.gnu.org/licenses>.