package Test::ManyParams; use 5.006; use strict; use warnings; require Exporter; our @ISA = qw(Exporter); our @EXPORT = qw/ all_ok all_are all_arent any_ok any_is any_isnt most_ok set_seed /; our $VERSION = '0.10'; use Test::Builder; use Set::CrossProduct; use Data::Dumper; our $seed; my $Tester = Test::Builder->new(); sub does_all { my ($sub, $params) = @_; my $failed_param = undef; if (ref($params->[0]) eq 'ARRAY') { if (grep {ref($params->[$_]) ne 'ARRAY'} (1 .. @$params-1)) { die "If the first parameter is an arrayref, all other parameters must be also. " . "Called with Parameter-Ref: " . _dump_params($params); } $failed_param = @$params > 1 ? _try_all_of_xproduct($sub, Set::CrossProduct->new( $params )) : _try_all_of_the_list($sub, @{$params->[0]}); } else { $failed_param = _try_all_of_the_list($sub, @$params); } my $ok = not defined($failed_param); my @diag = $ok ? () : ("Tests with the parameters: " . _dump_params($params), "Failed first using these parameters: " . _dump_params($failed_param)); return ($ok, @diag); } sub does_most { my ($sub, $params, $nr) = @_; my $failed_param = undef; $nr =~ /^\d+$/ or die "The number of tests that shall be done must be an integer, not '$nr'"; if (ref($params->[0]) eq 'ARRAY') { if (grep {ref($params->[$_]) ne 'ARRAY'} (1 .. @$params-1)) { die "If the first parameter is an arrayref, all other parameters must be also. " . "Called with Parameter-Ref: " . _dump_params($params); } $failed_param = @$params > 1 ? _try_most_of_xproduct($sub,$nr, Set::CrossProduct->new( $params )) : _try_most_of_the_list($sub,$nr, @{$params->[0]}); } else { $failed_param = _try_most_of_the_list($sub,$nr, @$params); } my $ok = not defined($failed_param); my @diag = $ok ? () : ("Tests with most ($nr) of the parameters: " . _dump_params($params), "Failed using these parameters: " . _dump_params($failed_param)); return ($ok, @diag); } sub all_ok(&$;$) { my ($sub, $params, $test_name) = @_; my ($ok, @diag) = does_all(@_); $Tester->ok( $ok, $test_name ) or do { $Tester->diag($_) for @diag }; return $ok; } sub most_ok(&$$;$) { my ($sub, $params, $nr, $test_name) = @_; my ($ok, @diag) = does_most(@_); $Tester->ok( $ok, $test_name ) or do { $Tester->diag($_) for @diag }; return $ok; } sub any_ok(&$;$) { my ($sub, $params, $test_name) = @_; # Please recognise the logic # To find out if any of the tests is O.K., # I ask whether all tests fail # If so there isn't any_ok, otherwise there is at least one ok my ($all_arent_ok) = does_all(sub {!$sub->(@_)}, $params, $test_name); my $ok = !$all_arent_ok; $Tester->ok( $ok, $test_name ); return $ok; } sub any_is(&$$;$) { my ($sub, $expected_value, $params, $test_name) = @_; my ($all_arent_ok, @diag) = does_all(sub {!($sub->(@_) eq $expected_value)}, $params, $test_name); my $ok = !$all_arent_ok; $Tester->ok( $ok, $test_name) or do { $Tester->diag($_) for @diag; $Tester->diag("Expected: " . _dump_params($expected_value)); $Tester->diag("but didn't found it with at least one parameter"); }; } sub any_isnt(&$$;$) { my ($sub, $expected_value, $params, $test_name) = @_; my ($all_arent_ok, @diag) = does_all(sub {!($sub->(@_) ne $expected_value)}, $params, $test_name); my $ok = !$all_arent_ok; $Tester->ok( $ok, $test_name) or do { $Tester->diag($_) for @diag; $Tester->diag("Expected to find any parameter where result is different to " . _dump_params($expected_value)); $Tester->diag("but didn't found such parameters"); }; } sub all_are(&$$;$) { my ($sub, $expected, $params, $test_name) = @_; my $found = undef; my ($ok, @diag) = does_all( sub { $found = $sub->(@_); $found eq $expected }, $params); $Tester->ok($ok, $test_name) or do { $Tester->diag($_) for @diag; $Tester->diag("Expected: " . _dump_params($expected)); $Tester->diag("but found: " . _dump_params($found)); }; } sub all_arent(&$$;$) { my ($sub, $unexpected, $params, $test_name) = @_; my $found = undef; my ($ok, @diag) = does_all( sub { $found = $sub->(@_); $found ne $unexpected }, $params); $Tester->ok($ok, $test_name) or do { $Tester->diag($_) for @diag; $Tester->diag("Expected not to find " . _dump_params($unexpected) . " but found it"); }; } sub _try_all_of_the_list { my ($sub, @param) = @_; foreach my $p (@param) { local $_ = $p; $sub->($_) or return [$_]; } return undef; } sub _try_most_of_the_list { my ($sub,$nr,@param) = @_; while ($nr-- > 0) { local $_ = $param[rand @param]; $sub->($_) or return [$_]; } return undef; } sub _try_all_of_xproduct { my ($sub, $iterator) = @_; my $tuple = undef; while ($tuple = $iterator->get()) { $sub->(@$tuple) or last; } return $tuple; } sub _try_most_of_xproduct { my ($sub, $nr, $iterator) = @_; while ($nr-- > 0) { my $tuple = $iterator->random; $sub->(@$tuple) or return $tuple; } return undef; } sub _dump_params { local $_ = Dumper($_[0]); s/\s+//gs; # remove all indents, but I didn't want to set # $Data::Dumper::Indent as it could have global effects s/^.*? = //; # remove the variable name of the dumped output return $_; } sub import { my @import_arg; my $seed_now = time ^ $$; # default value for the seed while (local $_ = shift @_) { /seed/ && do { $seed_now = shift(); $seed_now =~ /^\d+$/ or die "The seed must be an integer"; } or "DEFAULT" && push @import_arg, $_; } srand($seed_now); #Readonly::Scalar $seed => $seed_now; $seed = $seed_now; Test::ManyParams->export_to_level(1, @import_arg); } 1; __END__ =head1 NAME Test::ManyParams - module to test many params as one test =head1 SYNOPSIS use Test::ManyParams; all_ok {foo(@_)} [ [$arg1a, $arg2a], [$arg2b, $arg2b, $arg3b, $arg4b] ], "Testing that foo returns true for every combination of the arguments"; all_ok {bar(shift())} [qw/arg1 arg2 arg3 arg4 arg5 arg6/], "Testing every argument with bar"; all_are CODE VALUE, PARAMETERS, [ TEST_NAME ] all_arent CODE VALUE, PARAMETERS, [ TEST_NAME ] any_ok {drunken_person() eq shift()} ["Jim Beam", "Jonny Walker", "Jack Daniels"]; any_is {ask_for_sense_of_life(shift())} 42, ["Jack London", "Douglas Adams"]; most_ok {$img->colorAt($_[0],$_[1]) == BLACK} [ [0 .. 10_000], [0 .. 10_000] ] => 100, "100 random pixels of a black image should be black"; [NOT YET IMPLEMENTED] all_are_deeply CODE SCALAR, PARAMETERS, [ TEST_NAME ] all_like CODE REGEXP, PARAMETERS, [ TEST_NAME ] all_unlike CODE REGEXP, PARAMETERS, [ TEST_NAME ] all_can CODE METHODS, PARAMETERS, [ TEST_NAME ] all_dies_ok CODE PARAMETERS, [TEST_NAME] all_lives_ok CODE PARAMETERS, [TEST_NAME] all_throws_ok CODE REGEXP, PARAMETERS, [TEST_NAME] =head1 DESCRIPTION =head2 GENERAL PRINCIPLES This module helps to tests many parameters at once. In general, it calls the given subroutine with every combination of the given parameter values. The combinations are created with building a cross product. Especially it avoids writing ugly, boring code like: my $ok = 1; foreach my $x ($arg1a, $arg2a) { foreach my $y ($arg2a, $arg2b, $arg3b, $arg4b) { $ok &&= foo($x,$y); } } ok $ok, $testname; Instead you simpler write all_ok {foo(@_)} [ [$arg1a, $arg2a], [$arg2b, $arg2b, $arg3b, $arg4b] ] $testname; Additionally the output contains also some useful information about the parameters that should be tested and the first parameters the test failed. E.g. all_ok {$_[0] != 13 and $_[1] != 13} [ [1 .. 100], [1 .. 100] ], "No double bad luck"; would print: not ok 1 - No double bad luck # Failed test (x.pl at line 5) # Tests with the parameters: $VAR1=[[10,11,12,13,14,15],[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]]; # Failed first using these parameters: $VAR1=[10,13]; The parameters passed to C can be passed in two ways. If you need to test a crossproduct of more than one parameterlist, you have to write it as all_ok CODE [ \@arglist1, \@arglist2, ..., \@arglistn ], TEST_NAME; The CODE-routine will be called with every combination of the arglists, passed as simple arguments. E.g. all_ok {foo(@_)} [ ["red", "green", "blue"], ["big", "medium", "little"] ]; would call foo("red","big"); foo("red","medium"); foo("red","little"); foo("green","big"); foo("green","medium"); foo("green","little"); foo("blue","big"); foo("blue","medium"); foo("blue","little"); Note, that the order of calling shouldn't play any role, as it could be changed in future versions without any notice about. Please always remember, that a crossproduct of the lists can be very, very big. So don't write something like C, as it would test 32_768_000_000_000 parameter combinations. If you only want to test one parameter with different values, you can write in general all_ok CODE \@values, TEST_NAME So C would call C. (In this way and only in this, C<$_> will be set to the passed argument. Thus it is C<$_ = $_[0]> in this special case. Reason for this behaviour is just convenience.) Please take care, that the first element of the values list isn't an array ref, as Test::ManyParams would assume that you want to test combinations of the above. If it is important to pass values that are array refs, you have to write it this way: my @values = ( [1 .. 10], [100 .. 110], [990 .. 1000] ); all_ok {&foo} [ [@values] ]; # calls foo(1), ... foo(10), foo(100), ..., foo(110), foo(990), ..., foo(1000) what is very different to all_ok {&foo} [ @values ]; # what would call foo(1,100,900), foo(1,100,901), ... Of course, the test name is always optional, but recommended. =head2 FUNCTIONS =over =item all_ok CODE PARAMETERS, [ TEST_NAME ] See the general comments. =item all_are CODE VALUE, PARAMETERS, [ TEST_NAME ] The equivalent to C's C method. The given subroutine has to return always the given value. They are compared with 'eq'. =item all_arent CODE VALUE, PARAMETERS, [ TEST_NAME ] The equivalent to C's C method. The given subroutine has to return always values different from the given one. They are compared with 'eq'. =item any_ok CODE PARAMETERS, [ TEST_NAME ] Returns whether the subroutine returns true for one of the given parameters. =item any_is CODE VALUE, PARAMETERS, [ TEST_NAME ] Returns whether there is at least one parameter (combination) for that the given subroutine results the specified value. =item any_isnt CODE VALUE, PARAMETERS, [ TEST_NAME ] Returns whether there is at least one parameter (combination) for that the given subroutine results to a value different to the specified one. =item most_ok CODE PARAMETERS => NR_OF_TESTS [, TEST_NAME] Tests NR_OF_TESTS (randomly choosen) parameter (combinations) of the given parameters. All the (randomly choosen) parameters have to let the subroutine results to true. This method is intended to used, when a full test would need to long and you want to avoid systematic mistakes. There could be some parameter (combinations), that are tested twice or more often. =back =head2 IMPORTING In the most cases, you will simply import this module (C). But when you want to set the seed for the randomization of the C method, the way for importing looks like C 42>. At the time, it only will call C, but later on, it will hold foreach test script and file it's own random numbers, so that several modules all using this Test::ManyParams module won't collide. If you don't seet a seed value, at default the C