1 package t::lib::TestBuilder;
11 use Bytes::Random::Secure;
17 SIZE_BARCODE => 20, # Not perfect but avoid to fetch the value when creating a new item
23 bless( $self, $class );
25 $self->schema( Koha::Database->new()->schema );
26 $self->schema->storage->sql_maker->quote_char('`');
28 $self->{gen_type} = _gen_type();
29 $self->{default_values} = _gen_default_values();
34 my ($self, $schema) = @_;
36 if( defined( $schema ) ) {
37 $self->{schema} = $schema;
39 return $self->{schema};
42 # sub clear has been obsoleted; use delete_all from the schema resultset
45 my ( $self, $params ) = @_;
46 my $source = $params->{source} || return;
47 my @recs = ref( $params->{records} ) eq 'ARRAY'?
48 @{$params->{records}}: ( $params->{records} // () );
49 # tables without PK are not supported
50 my @pk = $self->schema->source( $source )->primary_columns;
53 foreach my $rec ( @recs ) {
54 # delete only works when you supply full primary key values
55 # $cond does not include searches for undef (not allowed in PK)
56 my $cond = { map { defined $rec->{$_}? ($_, $rec->{$_}): (); } @pk };
57 next if keys %$cond < @pk;
58 $self->schema->resultset( $source )->search( $cond )->delete;
59 # we clear the pk columns in the supplied hash
60 # this indirectly signals at least an attempt to delete
61 map { delete $rec->{$_}; } @pk;
68 my ( $self, $params ) = @_;
70 my $class = $params->{class};
71 my $value = $params->{value};
73 if ( not defined $class ) {
74 carp "Missing class param";
79 my $source = $class->_type;
80 my @pks = $self->schema->source( $class->_type )->primary_columns;
82 my $hashref = $self->build({ source => $source, value => $value });
85 foreach my $pk ( @pks ) {
86 push @ids, $hashref->{ $pk };
89 my $object = $class->find( @ids );
95 # build returns a hash of column values for a created record, or undef
96 # build does NOT update a record, or pass back values of an existing record
97 my ($self, $params) = @_;
98 my $source = $params->{source};
100 carp "Source parameter not specified!";
103 my $value = $params->{value};
105 my @unknowns = grep( !/^(source|value)$/, keys %{ $params });
106 carp "Unknown parameter(s): ", join( ', ', @unknowns ) if scalar @unknowns;
108 my $col_values = $self->_buildColumnValues({
112 return if !$col_values; # did not meet unique constraints?
114 # loop thru all fk and create linked records if needed
115 # fills remaining entries in $col_values
116 my $foreign_keys = $self->_getForeignKeys( { source => $source } );
117 for my $fk ( @$foreign_keys ) {
118 # skip when FK points to itself: e.g. borrowers:guarantorid
119 next if $fk->{source} eq $source;
120 my $keys = $fk->{keys};
121 my $tbl = $fk->{source};
122 my $res = $self->_create_links( $tbl, $keys, $col_values, $value );
123 return if !$res; # failed: no need to go further
124 foreach( keys %$res ) { # save new values
125 $col_values->{$_} = $res->{$_};
129 # store this record and return hashref
130 return $self->_storeColumnValues({
132 values => $col_values,
136 sub build_sample_biblio {
137 my ( $self, $args ) = @_;
139 my $title = $args->{title} || 'Some boring read';
140 my $author = $args->{author} || 'Some boring author';
141 my $frameworkcode = $args->{frameworkcode} || '';
142 my $itemtype = $args->{itemtype}
143 || $self->build_object( { class => 'Koha::ItemTypes' } )->itemtype;
145 my $marcflavour = C4::Context->preference('marcflavour');
147 my $record = MARC::Record->new();
148 my ( $tag, $subfield ) = $marcflavour eq 'UNIMARC' ? ( 200, 'a' ) : ( 245, 'a' );
149 $record->append_fields(
150 MARC::Field->new( $tag, ' ', ' ', $subfield => $title ),
153 ( $tag, $subfield ) = $marcflavour eq 'UNIMARC' ? ( 200, 'f' ) : ( 100, 'a' );
154 $record->append_fields(
155 MARC::Field->new( $tag, ' ', ' ', $subfield => $author ),
158 ( $tag, $subfield ) = $marcflavour eq 'UNIMARC' ? ( 995, 'r' ) : ( 942, 'c' );
159 $record->append_fields(
160 MARC::Field->new( $tag, ' ', ' ', $subfield => $itemtype )
163 my ($biblio_id) = C4::Biblio::AddBiblio( $record, $frameworkcode );
164 return Koha::Biblios->find($biblio_id);
167 sub build_sample_item {
168 my ( $self, $args ) = @_;
171 delete $args->{biblionumber} || $self->build_sample_biblio->biblionumber;
172 my $library = delete $args->{library}
173 || $self->build_object( { class => 'Koha::Libraries' } )->branchcode;
175 my $itype = delete $args->{itype}
176 || $self->build_object( { class => 'Koha::ItemTypes' } )->itemtype;
179 exists( $args->{barcode} )
181 : $self->_gen_text( { info => { size => SIZE_BARCODE } } );
183 my ( undef, undef, $itemnumber ) = C4::Items::AddItem(
185 homebranch => $library,
186 holdingbranch => $library,
193 return Koha::Items->find($itemnumber);
196 # ------------------------------------------------------------------------------
197 # Internal helper routines
200 # returns undef for failure to create linked records
201 # otherwise returns hashref containing new column values for parent record
202 my ( $self, $linked_tbl, $keys, $col_values, $value ) = @_;
205 my ( $cnt_scalar, $cnt_null ) = ( 0, 0 );
207 # First, collect all values for creating a linked record (if needed)
208 foreach my $fk ( @$keys ) {
209 my ( $col, $destcol ) = ( $fk->{col_name}, $fk->{col_fk_name} );
210 if( ref( $value->{$col} ) eq 'HASH' ) {
211 # add all keys from the FK hash
212 $fk_value = { %{ $value->{$col} }, %$fk_value };
214 if( exists $col_values->{$col} ) {
215 # add specific value (this does not necessarily exclude some
216 # values from the hash in the preceding if)
217 $fk_value->{ $destcol } = $col_values->{ $col };
219 $cnt_null++ if !defined( $col_values->{$col} );
223 # If we saw all FK columns, first run the following checks
224 if( $cnt_scalar == @$keys ) {
225 # if one or more fk cols are null, the FK constraint will not be forced
226 return {} if $cnt_null > 0;
227 # does the record exist already?
228 return {} if $self->schema->resultset($linked_tbl)->find( $fk_value );
230 # create record with a recursive build call
231 my $row = $self->build({ source => $linked_tbl, value => $fk_value });
232 return if !$row; # failure
234 # Finally, only return the new values
236 foreach my $fk ( @$keys ) {
237 my ( $col, $destcol ) = ( $fk->{col_name}, $fk->{col_fk_name} );
238 next if exists $col_values->{ $col };
239 $rv->{ $col } = $row->{ $destcol };
241 return $rv; # success
246 my $source = $params->{source} || return;
247 $source =~ s|(\w+)$|$1|;
251 sub _buildColumnValues {
252 my ($self, $params) = @_;
253 my $source = _formatSource( $params ) || return;
254 my $original_value = $params->{value};
257 my @columns = $self->schema->source($source)->columns;
258 my %unique_constraints = $self->schema->source($source)->unique_constraints();
261 # we try max $build_value times if there are unique constraints
262 BUILD_VALUE: while ( $build_value ) {
263 # generate random values for all columns
264 for my $col_name( @columns ) {
265 my $valref = $self->_buildColumnValue({
267 column_name => $col_name,
268 value => $original_value,
270 return if !$valref; # failure
271 if( @$valref ) { # could be empty
272 # there will be only one value, but it could be undef
273 $col_values->{$col_name} = $valref->[0];
277 # verify the data would respect each unique constraint
278 # note that this is INCOMPLETE since not all col_values are filled
279 CONSTRAINTS: foreach my $constraint (keys %unique_constraints) {
282 my $constraint_columns = $unique_constraints{$constraint};
283 # loop through all constraint columns and build the condition
284 foreach my $constraint_column ( @$constraint_columns ) {
286 # if one column does not exist or is undef, skip it
287 # an insert with a null will not trigger the constraint
289 if !exists $col_values->{ $constraint_column } ||
290 !defined $col_values->{ $constraint_column };
291 $condition->{ $constraint_column } =
292 $col_values->{ $constraint_column };
294 my $count = $self->schema
295 ->resultset( $source )
296 ->search( $condition )
299 # no point checking more stuff, exit the loop
304 last; # you passed all tests
306 return $col_values if $build_value > 0;
308 # if you get here, we have a problem
309 warn "Violation of unique constraint in $source";
313 sub _getForeignKeys {
315 # Returns the following arrayref
316 # [ [ source => name, keys => [ col_name => A, col_fk_name => B ] ], ... ]
317 # The array gives source name and keys for each FK constraint
319 my ($self, $params) = @_;
320 my $source = $self->schema->source( $params->{source} );
322 my ( @foreign_keys, $check_dupl );
323 my @relationships = $source->relationships;
324 for my $rel_name( @relationships ) {
325 my $rel_info = $source->relationship_info($rel_name);
326 if( $rel_info->{attrs}->{is_foreign_key_constraint} ) {
327 $rel_info->{source} =~ s/^.*:://g;
328 my $rel = { source => $rel_info->{source} };
331 while( my ($col_fk_name, $col_name) = each(%{$rel_info->{cond}}) ) {
332 $col_name =~ s|self.(\w+)|$1|;
333 $col_fk_name =~ s|foreign.(\w+)|$1|;
335 col_name => $col_name,
336 col_fk_name => $col_fk_name,
339 # check if the combination table and keys is unique
340 # so skip double belongs_to relations (as in Biblioitem)
341 my $tag = $rel->{source}. ':'.
342 join ',', sort map { $_->{col_name} } @keys;
343 next if $check_dupl->{$tag};
344 $check_dupl->{$tag} = 1;
345 $rel->{keys} = \@keys;
346 push @foreign_keys, $rel;
349 return \@foreign_keys;
352 sub _storeColumnValues {
353 my ($self, $params) = @_;
354 my $source = $params->{source};
355 my $col_values = $params->{values};
356 my $new_row = $self->schema->resultset( $source )->create( $col_values );
357 return $new_row? { $new_row->get_columns }: {};
360 sub _buildColumnValue {
361 # returns an arrayref if all goes well
362 # an empty arrayref typically means: auto_incr column or fk column
363 # undef means failure
364 my ($self, $params) = @_;
365 my $source = $params->{source};
366 my $value = $params->{value};
367 my $col_name = $params->{column_name};
369 my $col_info = $self->schema->source($source)->column_info($col_name);
372 if( $col_info->{is_auto_increment} ) {
373 if( exists $value->{$col_name} ) {
374 warn "Value not allowed for auto_incr $col_name in $source";
377 # otherwise: no need to assign a value
378 } elsif( $col_info->{is_foreign_key} || _should_be_fk($source,$col_name) ) {
379 if( exists $value->{$col_name} ) {
380 if( !defined $value->{$col_name} && !$col_info->{is_nullable} ) {
381 # This explicit undef is not allowed
382 warn "Null value for $col_name in $source not allowed";
385 if( ref( $value->{$col_name} ) ne 'HASH' ) {
386 push @$retvalue, $value->{$col_name};
388 # sub build will handle a passed hash value later on
390 } elsif( ref( $value->{$col_name} ) eq 'HASH' ) {
391 # this is not allowed for a column that is not a FK
392 warn "Hash not allowed for $col_name in $source";
394 } elsif( exists $value->{$col_name} ) {
395 if( !defined $value->{$col_name} && !$col_info->{is_nullable} ) {
396 # This explicit undef is not allowed
397 warn "Null value for $col_name in $source not allowed";
400 push @$retvalue, $value->{$col_name};
401 } elsif( exists $self->{default_values}{$source}{$col_name} ) {
402 push @$retvalue, $self->{default_values}{$source}{$col_name};
404 my $data_type = $col_info->{data_type};
405 $data_type =~ s| |_|;
406 if( my $hdlr = $self->{gen_type}->{$data_type} ) {
407 push @$retvalue, &$hdlr( $self, { info => $col_info } );
409 warn "Unknown type $data_type for $col_name in $source";
417 # This sub is only needed for inconsistencies in the schema
418 # A column is not marked as FK, but a belongs_to relation is defined
419 my ( $source, $column ) = @_;
420 my $inconsistencies = {
421 'Item.biblionumber' => 1, #FIXME: Please remove me when I become FK
423 return $inconsistencies->{ "$source.$column" };
428 tinyint => \&_gen_int,
429 smallint => \&_gen_int,
430 mediumint => \&_gen_int,
431 integer => \&_gen_int,
432 bigint => \&_gen_int,
434 float => \&_gen_real,
435 decimal => \&_gen_real,
436 double_precision => \&_gen_real,
438 timestamp => \&_gen_datetime,
439 datetime => \&_gen_datetime,
443 varchar => \&_gen_text,
444 tinytext => \&_gen_text,
446 mediumtext => \&_gen_text,
447 longtext => \&_gen_text,
449 set => \&_gen_set_enum,
450 enum => \&_gen_set_enum,
452 tinyblob => \&_gen_blob,
453 mediumblob => \&_gen_blob,
455 longblob => \&_gen_blob,
460 my ($self, $params) = @_;
461 my $data_type = $params->{info}->{data_type};
464 if( $data_type eq 'tinyint' ) {
467 elsif( $data_type eq 'smallint' ) {
470 elsif( $data_type eq 'mediumint' ) {
473 elsif( $data_type eq 'integer' ) {
476 elsif( $data_type eq 'bigint' ) {
477 $max = 9223372036854775807;
479 return int( rand($max+1) );
483 my ($self, $params) = @_;
485 if( defined( $params->{info}->{size} ) ) {
486 $max = 10 ** ($params->{info}->{size}->[0] - $params->{info}->{size}->[1]);
488 return sprintf("%.2f", rand($max-0.1));
492 my ($self, $params) = @_;
493 return $self->schema->storage->datetime_parser->format_date(DateTime->now())
497 my ($self, $params) = @_;
498 return $self->schema->storage->datetime_parser->format_datetime(DateTime->now());
502 my ($self, $params) = @_;
503 # From perldoc String::Random
504 my $size = $params->{info}{size} // 10;
505 $size -= alt_rand(0.5 * $size);
506 my $regex = $size > 1
507 ? '[A-Za-z][A-Za-z0-9_]{'.($size-1).'}'
509 my $random = String::Random->new( rand_gen => \&alt_rand );
510 # rand_gen is only supported from 0.27 onward
511 return $random->randregex($regex);
514 sub alt_rand { #Alternative randomizer
516 my $random = Bytes::Random::Secure->new( NonBlocking => 1 );
517 my $r = $random->irand / 2**32;
518 return int( $r * $max );
522 my ($self, $params) = @_;
523 return $params->{info}->{extra}->{list}->[0];
527 my ($self, $params) = @_;;
531 sub _gen_default_values {
536 gonenoaddress => undef,
546 more_subfields_xml => undef,
554 rentalcharge_daily => 0,
555 rentalcharge_hourly => 0,
556 defaultreplacecost => 0,
574 t::lib::TestBuilder.pm - Koha module to create test records
578 use t::lib::TestBuilder;
579 my $builder = t::lib::TestBuilder->new;
581 # The following call creates a patron, linked to branch CPL.
582 # Surname is provided, other columns are randomly generated.
583 # Branch CPL is created if it does not exist.
584 my $patron = $builder->build({
585 source => 'Borrower',
586 value => { surname => 'Jansen', branchcode => 'CPL' },
591 This module automatically creates database records for you.
592 If needed, records for foreign keys are created too.
593 Values will be randomly generated if not passed to TestBuilder.
594 Note that you should wrap these actions in a transaction yourself.
600 my $builder = t::lib::TestBuilder->new;
602 Constructor - Returns the object TestBuilder
606 my $schema = $builder->schema;
608 Getter - Returns the schema of DBIx::Class
614 records => $patron, # OR: records => [ $patron, ... ],
617 Delete individual records, created by builder.
618 Returns the number of delete attempts, or undef.
622 $builder->build({ source => $source_name, value => $value });
624 Create a test record in the table, represented by $source_name.
625 The name is required and must conform to the DBIx::Class schema.
626 Values may be specified by the optional $value hashref. Will be
627 randomized otherwise.
628 If needed, TestBuilder creates linked records for foreign keys.
629 Returns the values of the new record as a hashref, or undef if
630 the record could not be created.
632 Note that build also supports recursive hash references inside the
633 value hash for foreign key columns, like:
635 column1 => 'some_value',
637 columnA => 'another_value',
640 The hash for fk_col2 here means: create a linked record with build
641 where columnA has this value. In case of a composite FK the hashes
644 Realize that passing primary key values to build may result in undef
645 if a record with that primary key already exists.
649 Given a plural Koha::Object-derived class, it creates a random element, and
650 returns the corresponding Koha::Object.
652 my $patron = $builder->build_object({ class => 'Koha::Patrons' [, value => { ... }] });
656 Yohann Dufour <yohann.dufour@biblibre.com>
658 Koha Development Team
662 Copyright 2014 - Biblibre SARL
666 This file is part of Koha.
668 Koha is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by
669 the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
671 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.
673 You should have received a copy of the GNU General Public License along with Koha; if not, see <http://www.gnu.org/licenses>.