File Coverage

blib/lib/Assert/Refute/T/Basic.pm
Criterion Covered Total %
statement 146 146 100.0
branch 99 108 91.6
condition 47 68 69.1
subroutine 29 29 100.0
pod 3 3 100.0
total 324 354 91.5


line stmt bran cond sub pod time code
1             package Assert::Refute::T::Basic;
2              
3 58     58   2130 use 5.006;
  58         204  
4 58     58   315 use strict;
  58         116  
  58         1352  
5 58     58   305 use warnings;
  58         121  
  58         2991  
6             our $VERSION = '0.1501';
7              
8             =head1 NAME
9              
10             Assert::Refute::T::Basic - a set of most common checks for Assert::Refute suite
11              
12             =head1 DESCRIPTION
13              
14             This module contains most common test conditions similar to those in
15             L, like C or C.
16              
17             They appear as both exportable functions in this module
18             and L itself
19             I as corresponding methods in L.
20              
21             =head1 FUNCTIONS
22              
23             All functions below are prototyped to be used without parentheses and
24             exported by default. Scalar context is imposed onto arguments, so
25              
26             is @foo, @bar;
27              
28             would actually compare arrays by length.
29              
30             If a C is in action, the results of each assertion
31             will be recorded there. See L for more.
32             If L is in action, a unit testing script is assumed.
33             If neither is true, an exception is thrown.
34              
35             In addition, a Cfunction_name> method with
36             the same signature is generated for each of them
37             (see L).
38              
39             =cut
40              
41 58     58   390 use Carp;
  58         110  
  58         3800  
42 58     58   398 use Scalar::Util qw(blessed looks_like_number refaddr);
  58         145  
  58         3275  
43 58     58   376 use parent qw(Exporter);
  58         182  
  58         342  
44              
45 58     58   3464 use Assert::Refute::Build;
  58         156  
  58         133637  
46             our @EXPORT = qw( diag note );
47             our @EXPORT_OK;
48              
49             =head2 is $got, $expected, "explanation"
50              
51             Check for equality, C equals C and nothing else.
52              
53             =cut
54              
55             build_refute is => sub {
56 47     47   105 my ($got, $exp) = @_;
57              
58 47 100 100     211 if (defined $got xor defined $exp) {
59 6         20 return "unexpected ". to_scalar($got, 0);
60             };
61              
62 41 100 100     252 return '' if !defined $got or $got eq $exp;
63 16         64 return sprintf "Got: %s\nExpected: %s"
64             , to_scalar($got, 0), to_scalar($exp, 0);
65             }, args => 2, export => 1;
66              
67             =head2 isnt $got, $expected, "explanation"
68              
69             The reverse of is().
70              
71             =cut
72              
73             build_refute isnt => sub {
74 7     7   12 my ($got, $exp) = @_;
75 7 100 100     29 return if defined $got xor defined $exp;
76 3 100 100     16 return "Unexpected: ".to_scalar($got)
77             if !defined $got or $got eq $exp;
78             }, args => 2, export => 1;
79              
80             =head2 ok $condition, "explanation"
81              
82             =cut
83              
84             build_refute ok => sub {
85 15     15   27 my $got = shift;
86              
87 15         52 return !$got;
88             }, args => 1, export => 1;
89              
90             =head2 use_ok $module, [ @arguments ]
91              
92             Check whether the module can be loaded correctly with given arguments.
93             This never dies, only returns a failure.
94              
95             =cut
96              
97             # TODO write it better
98             build_refute use_ok => sub {
99 3     3   10 my ($mod, @arg) = @_;
100 3         8 my $caller = caller(1);
101 3 100   1   223 eval "package $caller; use $mod \@arg; 1" and return ''; ## no critic
  1     1   8  
  1     1   2  
  1         7  
  1         9  
  1         2  
  1         5  
  1         8  
  1         2  
  1         5  
102 1   50     12 return "Failed to use $mod: ".($@ || "(unknown error)");
103             }, list => 1, export => 1;
104              
105             =head1 require_ok My::Module
106              
107             Require, but do not call import.
108             This never dies, only returns a failure.
109              
110             =cut
111              
112             build_refute require_ok => sub {
113 2     2   7 my ($mod, @arg) = @_;
114 2         54 my $caller = caller(1);
115 2 100       187 eval "package $caller; require $mod; 1" and return ''; ## no critic
116 1   50     13 return "Failed to require $mod: ".($@ || "(unknown error)");
117             }, args => 1, export => 1;
118              
119             =head2 cpm_ok $value1, 'operation', $value2, "explanation"
120              
121             Currently supported: C E= == != E= E>
122             C
123              
124             Fails if any argument is undefined.
125              
126             =cut
127              
128             my %compare;
129             $compare{$_} = eval "sub { return \$_[0] $_ \$_[1]; }" ## no critic
130             for qw( < <= == != >= > lt le eq ne ge gt );
131             my %numeric;
132             $numeric{$_}++ for qw( < <= == != >= > );
133              
134             build_refute cmp_ok => sub {
135 22     22   53 my ($x, $op, $y) = @_;
136              
137 22         43 my $fun = $compare{$op};
138 22 100       298 croak "cmp_ok(): Comparison '$op' not implemented"
139             unless $fun;
140              
141 21         32 my @missing;
142 21 100       59 if ($numeric{$op}) {
143 18 100 66     92 push @missing, '1 '.to_scalar($x).' is not numeric'
144             unless looks_like_number $x or blessed $x;
145 18 100 66     60 push @missing, '2 '.to_scalar($y).' is not numeric'
146             unless looks_like_number $y or blessed $y;
147             } else {
148 3 100       7 push @missing, '1 is undefined' unless defined $x;
149 3 50       8 push @missing, '2 is undefined' unless defined $y;
150             };
151              
152 21 100       74 return "cmp_ok '$op': argument ". join ", ", @missing
153             if @missing;
154              
155 18 100       455 return '' if $fun->($x, $y);
156 6         38 return "$x\nis not '$op'\n$y";
157             }, args => 3, export => 1;
158              
159             =head2 like $got, qr/.../, "explanation"
160              
161             =head2 like $got, "regex", "explanation"
162              
163             B L, accepts string argument just fine.
164              
165             If argument is plain scalar, it is anchored to match the WHOLE string,
166             so that C<"foobar"> does NOT match C<"ob">,
167             but DOES match C<".*ob.*"> OR C.
168              
169             =head2 unlike $got, "regex", "explanation"
170              
171             The exact reverse of the above.
172              
173             B L, accepts string argument just fine.
174              
175             If argument is plain scalar, it is anchored to match the WHOLE string,
176             so that C<"foobar"> does NOT match C<"ob">,
177             but DOES match C<".*ob.*"> OR C.
178              
179             =cut
180              
181             build_refute like => \&_like_unlike,
182             args => 2, export => 1;
183              
184             build_refute unlike => sub {
185 5     5   12 _like_unlike( $_[0], $_[1], 1 );
186             }, args => 2, export => 1;
187              
188             sub _like_unlike {
189 68     68   157 my ($str, $reg, $reverse) = @_;
190              
191 68 100       280 $reg = qr#^(?:$reg)$# unless ref $reg eq 'Regexp';
192             # retain compatibility with Test::More
193 68 100       197 return "got (undef), expecting ".($reverse ? "anything except" : "")."\n$reg"
    100          
194             if !defined $str;
195 61 100 100     638 return '' if $str =~ $reg xor $reverse;
196 17 100       127 return "$str\n".($reverse ? "unexpectedly matches" : "doesn't match")."\n$reg";
197             };
198              
199             =head2 can_ok
200              
201             =cut
202              
203             build_refute can_ok => sub {
204 5     5   12 my $class = shift;
205              
206 5 50       10 croak ("can_ok(): no methods to check!")
207             unless @_;
208              
209 5 50       13 return 'undefined' unless defined $class;
210 5 50       30 return 'Not an object: '.to_scalar($class)
211             unless UNIVERSAL::can( $class, "can" );
212              
213 5         11 my @missing = grep { !$class->can($_) } @_;
  8         41  
214 5   66     23 return @missing && (to_scalar($class, 0)." has no methods ".join ", ", @missing);
215             }, list => 1, export => 1;
216              
217             =head2 isa_ok
218              
219             =cut
220              
221             build_refute isa_ok => \&_isa_ok, args => 2, export => 1;
222              
223             build_refute new_ok => sub {
224 2     2   7 my ($class, $args, $target) = @_;
225              
226 2 50       6 croak ("new_ok(): at least one argument must be present")
227             unless defined $class;
228 2 50       7 croak ("new_ok(): too many arguments")
229             if @_ > 3;
230              
231 2   50     6 $args ||= [];
232 2   33     8 $class = ref $class || $class;
233 2   33     9 $target ||= $class;
234              
235 2 50       11 return "Not a class: ".to_scalar($class, 0)
236             unless UNIVERSAL::can( $class, "can" );
237 2 100       15 return "Class has no 'new' method: ".to_scalar( $class, 0 )
238             unless $class->can( "new" );
239              
240 1         5 return _isa_ok( $class->new( @$args ), $target );
241             }, list => 1, export => 1;
242              
243             sub _isa_ok {
244 5     5   11 my ($obj, $class) = @_;
245              
246 5 50       12 croak 'isa_ok(): No class supplied to check against'
247             unless defined $class;
248 5 50       12 return "undef is not a $class" unless defined $obj;
249 5   33     18 $class = ref $class || $class;
250              
251 5 100 66     55 if (
      66        
252             (UNIVERSAL::can( $obj, "isa" ) && !$obj->isa( $class ))
253             || !UNIVERSAL::isa( $obj, $class )
254             ) {
255 2         7 return to_scalar( $obj, 0 ) ." is not a $class"
256             };
257 3         11 return '';
258             };
259              
260             =head2 contract_is $contract, "signature", ["message"]
261              
262             Check that a contract has been fullfilled to exactly the specified extent.
263              
264             See L for exact signature format.
265              
266             =cut
267              
268             build_refute contract_is => sub {
269 35     35   88 my ($c, $sig) = @_;
270              
271 35         122 my $got = $c->get_sign;
272 35   66     174 return $got ne $sig && <<"EOF".$c->get_tap;
273             Unexpected subcontract signature.
274             Got: $got
275             Expected: $sig
276             Execution log:
277             EOF
278             }, args => 2, export => 1;
279              
280             =head2 diag @message
281              
282             Human-readable diagnostic message.
283              
284             References are automatically serialized to depth 1.
285              
286             =head2 note @message
287              
288             Human-readable comment message.
289              
290             References are automatically serialized to depth 1.
291              
292             =cut
293              
294             sub diag (@) { ## no critic
295 5     5 1 26 current_contract->diag(@_);
296             };
297              
298             sub note (@) { ## no critic
299 1     1 1 8 current_contract->note(@_);
300             };
301              
302             =head2 is_deeply( $got, $expected )
303              
304             my $check = contract {
305             my $arg = shift;
306             my $expected = naive_impl( $arg );
307             is_deeply fast_impl( $arg ), $expected, "fast_impl generates same data";
308             };
309              
310             Unlike the L counterpart, it will not first after first mismatch
311             and print details about 10 mismatching entries.
312              
313             =head2 is_deeply_diff( $got, $expected, $max_diff )
314              
315             Same as above, but the third parameter specifies the number
316             of mismatches in data to be reported.
317              
318             B<[EXPERIMENTAL]> name and meaning may change in the future.
319             a C<$max_diff> of 0 would lead to unpredictable results.
320              
321             =cut
322              
323             push @EXPORT_OK, qw(deep_diff);
324              
325             build_refute is_deeply => \&deep_diff, export => 1, args => 2;
326             build_refute is_deeply_diff => \&deep_diff, export_ok => 1, args => 3;
327              
328             =head2 deep_diff( $old, $new )
329              
330             Not exported by default.
331             Compares 2 scalars recursively, outputs nothing if they are identical,
332             or a I difference if they differ.
333              
334             The exact difference format shall not be relied upon.
335              
336             =cut
337              
338             sub deep_diff {
339 51     51 1 126 my ($old, $new, $maxdiff, $known, $path) = @_;
340              
341 51   100     170 $path ||= '$deep';
342 51 100       104 $maxdiff = 10 unless defined $maxdiff;
343              
344             # First compare types. Report if different.
345 51 100 50     300 if (ref $old ne ref $new or (defined $old xor defined $new)) {
      66        
346 1         4 return _deep_not($path, $old, $new);
347             };
348              
349             # Check scalar values. compare with eq.
350 50 100       107 if (!ref $old) {
351 12 100       59 return unless defined $old;
352 6 100       31 return $old eq $new
353             ? ()
354             : _deep_not($path, $old, $new),
355             };
356              
357             # Topology check (and also avoid infinite loop)
358             # If we've seen these structures before,
359             # just compare the place where it happened
360             # if not, remember for later
361             # From now on, $path eq $seen_* really means "never seen before"
362             # NOTE refaddr(...) to get rid of warning under older perls
363 38   66     206 my $seen_old = $known->{-refaddr($old)} ||= $path;
364 38   66     159 my $seen_new = $known->{ refaddr($new)} ||= $path;
365              
366             # Seen previously in different places - report
367 38 100       84 if ($seen_old ne $seen_new) {
368             # same as _deep_not, but with addresses
369             return [
370 4 100       27 "At $path: ",
    100          
371             " Got: ".($seen_old ne $path ? "Same as $seen_old" : to_scalar($old,2)),
372             "Expected: ".($seen_new ne $path ? "Same as $seen_new" : to_scalar($new,2)),
373             ];
374             };
375             # these structures have already been compared elsewhere - skip
376 34 100       84 return if $seen_old ne $path;
377              
378             # this is the same structure - skip
379 33 100       99 return if refaddr($old) eq refaddr($new);
380              
381             # descend into deep structures
382 30   50     68 $known ||= {};
383              
384 30 100       77 if (UNIVERSAL::isa( $old , 'ARRAY') ) {
385 16         22 my @diff;
386 16 100       45 my $min = @$old < @$new ? scalar @$old : scalar @$new;
387 16 100       43 my $max = @$old > @$new ? scalar @$old : scalar @$new;
388 16         42 foreach my $i( 0 .. $min - 1 ) {
389 21         97 my $off = deep_diff( $old->[$i], $new->[$i], $maxdiff, $known, $path."[$i]" );
390 21 100       51 if ($off) {
391 7         17 push @diff, @$off;
392 7         19 $maxdiff -= @$off / 3;
393             };
394 21 100       57 last if $maxdiff <= 0;
395             };
396 16         37 foreach my $i ($min .. $max - 1) {
397 3         15 push @diff, _deep_noexist( $path."[$i]", $old->[$i], $new->[$i], @$new - @$old );
398 3         10 $maxdiff--;
399 3 100       10 last if $maxdiff <= 0;
400             };
401 16 100       102 return @diff ? \@diff : ();
402             };
403              
404 14 100       39 if (UNIVERSAL::isa( $old, 'HASH') ) {
405 13         22 my %both;
406 13         68 $both{$_}-- for keys %$old;
407 13         56 $both{$_}++ for keys %$new;
408 13         24 my @diff;
409 13         52 foreach (sort keys %both) {
410 25 100       58 if ($both{$_}) {
411             # nonzero = only one side exists
412 15         66 push @diff, _deep_noexist( $path."{$_}", $old->{$_}, $new->{$_}, $both{$_} );
413 15         39 $maxdiff--;
414 15 100       44 last if $maxdiff <= 0;
415             } else {
416 10         77 my $off = deep_diff( $old->{$_}, $new->{$_}, $maxdiff, $known, $path."{$_}" );
417 10 100       29 if ($off) {
418 8         21 push @diff, @$off;
419 8         23 $maxdiff -= @$off/3;
420             };
421 10 100       32 last if $maxdiff <= 0;
422             };
423             };
424 13 100       129 return @diff ? \@diff : ();
425             };
426              
427             # finally - totally different - just output them
428 1         5 return _deep_not($path, $old, $new);
429             };
430              
431             sub _deep_not {
432 3     3   10 my ($path, $old, $new) = @_;
433             return [
434 3         13 "At $path: ",
435             " Got: ".to_scalar( $old, 2 ),
436             "Expected: ".to_scalar( $new, 2 ),
437             ];
438             };
439              
440             # $sign < 0 = $old exists, $sign > 0 $new exists
441             # $sign == 0 and see above
442             sub _deep_noexist {
443 18     18   60 my ($path, $old, $new, $sign) = @_;
444             # return array, not arrayref, as this is getting pushed
445             return (
446 18 100       77 "At $path: ",
    100          
447             " Got: ".($sign < 0 ? to_scalar( $old, 2 ) : "Does not exist"),
448             "Expected: ".($sign > 0 ? to_scalar( $new, 2 ) : "Does not exist"),
449             );
450             };
451              
452             =head2 pass "explanation"
453              
454             Declare the test ok. Synonym for ok(1)
455              
456             =cut
457              
458             build_refute pass => sub {
459 1     1   5 return 0;
460             }, args => 0, export => 1;
461              
462             =head2 fail "explanation"
463              
464             Declare the test not ok. Synonym for ok(0)
465              
466             =cut
467              
468             build_refute fail => sub {
469 1     1   12 return 1;
470             }, args => 0, export => 1;
471              
472             =head1 LICENSE AND COPYRIGHT
473              
474             This module is part of L suite.
475              
476             Copyright 2017-2018 Konstantin S. Uvarin. C<< >>
477              
478             This program is free software; you can redistribute it and/or modify it
479             under the terms of the the Artistic License (2.0). You may obtain a
480             copy of the full license at:
481              
482             L
483              
484             =cut
485              
486             1;