File Coverage

blib/lib/Test/PDL.pm
Criterion Covered Total %
statement 86 86 100.0
branch 64 70 91.4
condition 13 21 61.9
subroutine 11 11 100.0
pod 3 3 100.0
total 177 191 92.6


line stmt bran cond sub pod time code
1             package Test::PDL;
2              
3             =head1 NAME
4              
5             Test::PDL - Test Perl Data Language arrays (a.k.a. ndarrays) for equality
6              
7             =head1 SYNOPSIS
8              
9             use PDL;
10             use Test::More tests => 3;
11             use Test::PDL qw( is_pdl :deep );
12              
13             # an example of a test that succeeds
14             $got = sequence 5;
15             $expected = pdl( 0,1,2,3,4 );
16             is_pdl( $got, $expected, 'sequence() works as expected' );
17             # OUTPUT:
18             # ok 1 - sequence() works as expected
19              
20             # if a test fails, detailed diagnostics are printed; the output is
21             # similar to that of is() from L
22             $got = pdl( 0,-1,-2,3,4 );
23             $expected = sequence 5;
24             is_pdl( $got, $expected, 'demonstrate the output of a failing test' );
25             # OUTPUT:
26             # not ok 2 - demonstrate the output of a failing test
27             #
28             # Failed test 'demonstrate the output of a failing test'
29             # at maint/pod.t line 16.
30             # 2/5 values do not match
31             # got: Double D [5] (P ) [0 -1 -2 3 4]
32             # expected: Double D [5] (P ) [0 1 2 3 4]
33             # First <=5 values differ at:
34             # [
35             # [1]
36             # [2]
37             # ]
38             # Those 'got' values: [-1 -2]
39             # Those 'expected' values: [1 2]
40              
41             # ndarrays within other data structures can be tested with Test::Deep
42             use Test::Deep qw( cmp_deeply );
43             $got = { name => 'Histogram', data => long( 17,0,1 ) };
44             $expected = { name => 'Histogram', data => test_long( 17,0,0,1 ) };
45             cmp_deeply( $got, $expected, 'demonstrate the output of a failing deep comparison' );
46             # OUTPUT:
47             # not ok 3 - demonstrate the output of a failing deep comparison
48             #
49             # Failed test 'demonstrate the output of a failing deep comparison'
50             # at maint/pod.t line 30.
51             # Comparing $data->{"data"} as an ndarray:
52             # dimensions do not match in extent
53             # got : Long D [3] (P ) [17 0 1]
54             # expect : Long D [4] (P ) [17 0 0 1]
55              
56             =cut
57              
58 56     56   3735102 use strict;
  56         138  
  56         2595  
59 56     56   287 use warnings;
  56         109  
  56         3420  
60 56     56   25684 use PDL::Lite;
  56         210  
  56         654  
61 56     56   447 use PDL::Types ();
  56         108  
  56         1626  
62              
63 56     56   316 use base qw( Exporter );
  56         151  
  56         88348  
64             our @EXPORT = qw( is_pdl );
65             our @EXPORT_OK = qw( eq_pdl is_pdl test_pdl );
66             our %EXPORT_TAGS = ( deep => [ qw( test_pdl ) ] );
67             our $VERSION = '0.22';
68              
69             =head1 DESCRIPTION
70              
71             With Test::PDL, you can compare two ndarrays for equality. The comparison is
72             performed as thoroughly as possible, comparing types, dimensions, bad value
73             patterns, and finally the values themselves. The exact behaviour can be
74             configured by setting certain package-wide defaults (see %DEFAULTS below), or
75             by supplying options in a function call.
76             Test::PDL is mostly useful in test scripts.
77              
78             Test::PDL is to be used with the Perl Data Language (L).
79              
80             By default, Test::PDL exports only one function: is_pdl(). The other functions
81             are exported on demand only. The export tag C<:deep> exports test_pdl() and one
82             function for each PDL type constructor (like short(), double(), etc.), prefixed
83             with C: test_short(), test_double(), ...
84              
85             =head1 VARIABLES
86              
87             =head2 %DEFAULTS
88              
89             The default comparison criteria used by Test::PDL can be configured by setting
90             the values in the %DEFAULTS hash. This can be done directly, by addressing
91             %Test::PDL::DEFAULTS directly.
92              
93             =over 4
94              
95             =item atol
96              
97             The absolute tolerance used to compare values. Initially set to 1e-6.
98              
99             =item require_equal_types
100              
101             If true, only ndarrays with equal type can be considered equal. If false, the
102             types of the ndarrays being compared is not taken into consideration. Defaults
103             to true: types must match for the comparison to succeed. If you want to
104             write tests like
105              
106             is_pdl( $got, pdl([ 1, 3, 5, 6 ]) );
107              
108             without having to worry about the type of the ndarray being exactly I
109             (which is the default type of the pdl() constructor), set I equal to
110             0.
111              
112             =item rtol
113              
114             The relative tolerance used to compare values. Initially set to 1e-6.
115              
116             =back
117              
118             =cut
119              
120             our %DEFAULTS = (
121             atol => 1e-6,
122             require_equal_types => 1,
123             rtol => 1e-6,
124             );
125              
126             =head1 FUNCTIONS
127              
128             =head2 import
129              
130             Custom importer that recognizes configuration defaults specified at use time, as
131             in
132              
133             use Test::PDL -require_equal_types => 0;
134              
135             =cut
136              
137             sub import
138             {
139 62     62   30867 my $i = 0;
140 62         318 while( $i < @_ ) {
141 81 100       376 if( $_[ $i ] =~ /^-/ ) {
142 15         47 my( $key, $val ) = splice @_, $i, 2;
143 15         96 $key =~ s/^-(.*)/$1/;
144 15 100       56 PDL::barf( "invalid name $key" ) unless grep { $key eq $_ } keys %DEFAULTS;
  45         119  
145 14 50       76 PDL::barf( "undefined value for $key" ) unless defined $val;
146 14         46 $DEFAULTS{ $key } = $val;
147             }
148 66         198 else { $i++ }
149             }
150 61         1351194 __PACKAGE__->export_to_level( 1, @_ );
151             }
152              
153             =head2 is_pdl
154              
155             =for ref # PDL
156              
157             Run a test comparing an ndarray to an expected ndarray, and fail with detailed
158             diagnostics if they don't compare equal.
159              
160             =for usage # PDL
161              
162             is_pdl( $got, $expected );
163             is_pdl( $got, $expected, $test_name );
164             is_pdl( $got, $expected, { test_name => $test_name } );
165             is_pdl( $got, $expected, { atol => $absolute_tolerance, ... } );
166              
167             Yields ok if the first two arguments are ndarrays that compare equal, not ok if
168             the ndarrays are different, or if at least one is not an ndarray. Prints a
169             diagnostic when the comparison fails, with the reason and a brief printout of
170             both arguments. See the documentation of eq_pdl() for the comparison
171             criteria. $test_name is optional.
172              
173             Named after is() from L.
174              
175             =cut
176              
177             sub is_pdl {
178 1519     1519 1 228626 require Test::Builder;
179 1519         7312 my $tb = Test::Builder->new;
180 1519 100       13394 $tb->croak('error in arguments: > 3 given') if @_ > 3;
181 1518         4075 my ( $got, $expected, $arg ) = @_;
182             $tb->croak('error in arguments: third argument is an ndarray')
183 1518 100       2696 if eval { $arg->isa('PDL') };
  1518         13807  
184 1517         8089 my $opt = { %DEFAULTS };
185 1517         3040 my $name;
186 1517 100       3565 if ($arg) {
187 1265 100       3224 if (ref $arg eq 'HASH') { $opt = { %$opt, %$arg } }
  81         478  
188 1184         2016 else { $name = $arg }
189             }
190 1517   100     5272 $name ||= $opt->{test_name} || "ndarrays are equal";
      66        
191 1517         4283 my ($ok, $reason, $mask) = eq_pdl($got, $expected, $opt);
192 1517 100       10369 return $tb->ok(1, $name) if $ok;
193 15         60 my $rc = $tb->ok( 0, $name );
194 15         18351 my $fmt = '%-8T %-12D (%-5S) ';
195 15         30 my @mismatch;
196 15 100       51 if (defined $mask) {
197 7 50       279 my $coords = defined $mask ? $mask->not->whichND : undef;
198 7 50 33     73 $coords = $coords->slice(',0:4') if defined $coords and $coords->dim(1) > 5;
199 7         29 my $cstr = $coords->string; $cstr =~ s#\n+\z##;
  7         58  
200 7         42 push @mismatch, (
201             "\nFirst <=5 values differ at: $cstr\n",
202             "Those 'got' values: ", $got->indexND($coords),
203             "\nThose 'expected' values: ", $expected->indexND($coords),
204             );
205             }
206             $tb->diag(
207             " $reason\n",
208 15 100       182 " got: ", eval { $got->isa('PDL') && !$got->isnull } ? $got->info( $fmt ) : '', $got, "\n",
209 15 100       48 " expected: ", eval { $expected->isa('PDL') && !$expected->isnull } ? $expected->info( $fmt ) : '', $expected,
  15 100       167  
    100          
210             @mismatch,
211             );
212 15         8858 return $rc;
213             }
214              
215             =head2 eq_pdl
216              
217             =for ref # PDL
218              
219             Return true if two ndarrays compare equal, false otherwise. In list context,
220             additionally returns a diagnostic string.
221              
222             =for usage # PDL
223              
224             my $equal = eq_pdl( $got, $expected );
225             my $equal = eq_pdl( $got, $expected, { atol => $absolute_tolerance, ... } );
226             my( $equal, $diag ) = eq_pdl( $got, $expected );
227             my( $equal, $diag ) = eq_pdl( $got, $expected, { atol => $absolute_tolerance, ... } );
228              
229             eq_pdl() contains just the comparison part of is_pdl(), without the
230             infrastructure required to write tests with L. It could be used as
231             part of a larger test in which the equality of two ndarrays must be verified. By
232             itself, eq_pdl() does not generate any output, so it should be safe to use
233             outside test suites.
234              
235             In list context, eq_pdl() returns a list with three elements, the first one being
236             a boolean whether the ndarrays compared equal, the second being a diagnostic
237             string explaining why the comparison failed (or the empty string, if it didn't
238             fail). The third is either the mask of not-equal if the values didn't
239             match, or C.
240             This is useful in combination with L, but might also be
241             useful on its own.
242              
243             eq_pd() does not need L, so you can use it as part of something
244             else, without side effects (like generating output).
245              
246             The criteria for equality are the following:
247              
248             =over 4
249              
250             =item *
251              
252             Both arguments must be ndarrays for the comparison to succeed. Currently, there
253             is no implicit conversion from scalar to ndarray.
254              
255             =item *
256              
257             The type of both ndarrays must be equal if (and only if) I is true.
258              
259             =item *
260              
261             The number of dimensions must be equal. That is, a two-dimensional ndarray only
262             compares equal with another two-dimensional ndarray.
263              
264             =item *
265              
266             The extent of the dimensions are compared one by one and must match. That is, a
267             ndarray with dimensions (5,4) cannot compare equal with an ndarray of dimensions
268             (5,3). Note that degenerate dimensions are not treated specially, and thus a
269             ndarray with dimensions (5,4,1) is considered different from an ndarray with
270             dimensions (5,4).
271              
272             =item *
273              
274             For ndarrays that conform in type and shape, the bad value pattern is examined.
275             If the two ndarrays have bad values in different positions, the ndarrays are
276             considered different. Note that two ndarrays may compare equal even though their
277             bad flag is different, if there are no bad values.
278              
279             =item *
280              
281             And last but not least, the values themselves are examined one by one.
282             As of 0.21, both integer and floating-point types are compared approximately.
283             The approximate comparison is
284             implemented using a combination of relative and absolute tolerances, which can
285             be set by supplying an argument to C, or by supplying an
286             optional hash to this function. By default, the absolute and relative
287             tolerances are both equal to 1e-6. The user can specify a pure relative
288             tolerance by specifying C 0>, and a pure absolute tolerance by
289             specifying C 0>. If both tolerances are specified, values compare
290             equal if I their difference is lower than or equal to the absolute
291             tolerance I their relative difference (with respect to the expected
292             value) is lower than or equal to the relative tolerance. For expected
293             values equal to zero, relative differences (with respect to the expected
294             value) make no sense, and the use of combined absolute and relative
295             tolerances is recommended.
296              
297             =back
298              
299             =cut
300              
301             sub eq_pdl {
302 1771     1771 1 312218 my ($got, $expected, $arg) = @_;
303 1771 100       10453 my $opt = { %DEFAULTS, ref $arg eq 'HASH' ? %$arg : () };
304 1771 50 33     7048 PDL::barf( 'need an absolute or a relative tolerance, or both' ) unless defined $opt->{atol} || defined $opt->{rtol};
305 1771   50     5175 $opt->{atol} //= 0;
306 1771   50     4346 $opt->{rtol} //= 0;
307 1771 50       4778 PDL::barf('absolute tolerance cannot be negative') if $opt->{atol} < 0;
308 1771 50       4244 PDL::barf('relative tolerance cannot be negative') if $opt->{rtol} < 0;
309             return wantarray ? (0, 'received value is not an ndarray', undef) : 0
310 1771 100       2836 if !eval { $got->isa('PDL') };
  1771 100       9101  
311             return wantarray ? (0, 'expected value is not an ndarray', undef) : 0
312 1761 100       3362 if !eval { $expected->isa('PDL') };
  1761 100       5622  
313             return wantarray ? (0, 'types do not match (\'require_equal_types\' is true)', undef) : 0
314 1756 100 100     9355 if $opt->{require_equal_types} && $got->type != $expected->type;
    100          
315 1750         10088 my @got_dims = $got->dims;
316 1750         4797 my @exp_dims = $expected->dims;
317 1750 100       4446 return wantarray ? (0, 'dimensions do not match in number', undef) : 0
    100          
318             if @got_dims != @exp_dims;
319 1744         4185 while (@got_dims) {
320 2054 100       7501 return wantarray ? (0, 'dimensions do not match in extent', undef) : 0
    100          
321             if shift(@got_dims) != shift(@exp_dims);
322             }
323 1733 100 66     5585 return wantarray ? (1, '', undef) : 1
    100          
324             if $got->isempty and $expected->isempty;
325             # both are now non-empty
326 1671         106208 my $res = PDL::Primitive::approx_artol( $got, $expected, @$opt{qw(atol rtol)} );
327 1671 100       8924 return wantarray ? (1, '', undef) : 1 if $res->all;
    100          
328 44         513 my $exp_nelem = $expected->nelem;
329 44         184 my $reason = ($exp_nelem-$res->sum)."/$exp_nelem values do not match";
330 44 100       493 return wantarray ? (0, $reason, $res) : 0;
331             }
332              
333             =head2 test_pdl
334              
335             =for ref # PDL
336              
337             Special comparison to be used in conjunction with L to test ndarrays
338             inside data structures.
339              
340             =for usage # PDL
341              
342             my $expected = { ..., some_field => test_pdl( 1,2,-7 ), ... };
343             my $expected = [ ..., test_short( 1,2,-7 ), ... ];
344              
345             Suppose you want to compare data structures that happen to contain ndarrays. You
346             use is_deeply() (from L) or cmp_deeply() (from L) to
347             compare the structures element by element. Unfortunately, you cannot just write
348              
349             my $got = my_sub( ... );
350             my $expected = {
351             ...,
352             some_field => pdl( ... ),
353             ...
354             };
355             is_deeply $got, $expected;
356              
357             Neither does cmp_deeply() work in the same situation. is_deeply() tries to
358             compare the ndarrays using the (overloaded) C<==> comparison operator, which
359             doesn't work. It simply dies with an error message saying that multidimensional
360             ndarrays cannot be compared, whereas cmp_deeply() performs only a shallow
361             comparison of the references.
362              
363             What you need is a special comparison, which is provided by this function, to
364             be used with cmp_deeply(). You need to rewrite $expected as follows
365              
366             my $expected = {
367             ...,
368             some_field => test_pdl( ... ),
369             ...
370             };
371             cmp_deeply $got, $expected;
372              
373             Note that you need to write test_pdl() instead of pdl(). You could achieve the
374             same thing with
375              
376             my $expected = {
377             ...,
378             some_field => code( sub { eq_pdl( shift, pdl( ... ) ) } ),
379             ...
380             };
381              
382             but the diagnostics provided by test_pdl() are better, and it's easier to use.
383             test_pdl() accepts the same arguments as the PDL constructor pdl() does. If you
384             need to compare an ndarray with a type different from the default type, use one
385             of the provided test_byte(), test_short(), test_long(), etc.:
386              
387             my $expected = { data => test_short( -4,-9,13 ) };
388              
389             If you need to manipulate the expected value, you should keep in mind that the
390             return value of test_pdl() and the like are not ndarrays. Therefore, in-place
391             modification of the expected value won't work:
392              
393             my $expected = { data => test_short( -99,-9,13 )->inplace->setvaltobad( -99 ) }; # won't work!
394              
395             You should rather do
396              
397             my $expected = { data => test_pdl( short(-99,-9,13)->inplace->setvaltobad(-99) ) };
398              
399             test_pdl() will correctly set the type of the expected value to I in the
400             above example.
401              
402             =cut
403              
404             sub test_pdl
405             {
406 49     49 1 1529 require Test::Deep::PDL;
407 49         316 my $expected = PDL::Core::pdl( @_ );
408 49         301 return Test::Deep::PDL->new( $expected );
409             }
410              
411             =for Pod::Coverage test_anyval test_byte test_short test_ushort test_long
412             test_indx test_longlong test_float test_double test_cfloat test_cdouble
413             test_cldouble test_ldouble test_sbyte test_ulong test_ulonglong
414              
415             =cut
416              
417             for my $type ( PDL::Types::types ) {
418             my $sub = sub {
419 60     60   736 require Test::Deep::PDL;
420 60 100       496 my $expected = PDL::convert(
421             PDL::Core::alltopdl( 'PDL', scalar(@_) > 1 ? [@_] : shift ),
422             $type->numval
423             );
424 60         706 return Test::Deep::PDL->new( $expected );
425             };
426             my $sub_name = 'test_' . $type->convertfunc;
427             {
428 56     56   491 no strict 'refs';
  56         123  
  56         6323  
429             *$sub_name = $sub;
430             }
431             push @EXPORT_OK, $sub_name;
432             push @{ $EXPORT_TAGS{deep} }, $sub_name;
433             }
434              
435             =head1 BUGS
436              
437             None reported so far.
438              
439             =head1 SEE ALSO
440              
441             L, L, L, L
442              
443             =head1 ACKNOWLEDGMENTS
444              
445             Thanks to PDL Porters Joel Berger, Chris Marshall, and David Mertens for
446             feedback and improvements.
447              
448             Thanks to Ed J, Zakariyya Mughal, and Diab Jerius for feedback, improvements,
449             maintenance of the code, and encouragement!
450              
451             =cut
452              
453             1;