File Coverage

blib/lib/Assert/Refute/Build.pm
Criterion Covered Total %
statement 92 92 100.0
branch 46 50 92.0
condition 21 31 67.7
subroutine 15 15 100.0
pod 3 3 100.0
total 177 191 92.6


line stmt bran cond sub pod time code
1             package Assert::Refute::Build;
2              
3 63     63   70960 use 5.006;
  63         238  
4 63     63   321 use strict;
  63         154  
  63         1359  
5 63     63   325 use warnings;
  63         119  
  63         3405  
6             our $VERSION = '0.17';
7              
8             =head1 NAME
9              
10             Assert::Refute::Build - tool for extending Assert::Refute suite
11              
12             =head1 DESCRIPTION
13              
14             Although arbitrary checks may be created using just the C function,
15             they may be cumbersome to use and especially share.
16              
17             This module takes care of some boilerplate as well as maintains parity
18             between functional and object-oriented interfaces of L.
19              
20             =head1 SYNOPSIS
21              
22             Extending the test suite goes as follows:
23              
24             package My::Package;
25             use Assert::Refute::Build;
26             use parent qw(Exporter);
27              
28             build_refute is_everything => sub {
29             return if $_[0] == 42;
30             return "$_[0] is not answer to life, universe, and everything";
31             }, export => 1, args => 1;
32              
33             1;
34              
35             This can be later used inside production code to check a condition:
36              
37             use Assert::Refute qw(:all);
38             use My::Package;
39             my $fun_check = contract {
40             is_everything( shift );
41             };
42             my $oo_check = contract {
43             $_[0]->is_everything( $_[1] );
44             }, need_object => 1;
45             # ditto
46              
47             # apply $fun_check or $oo_check to a variable, get result
48              
49             my $log = $oo_check->apply(137);
50             $log->is_passing; # nope
51             $log->get_tap; # get details
52              
53             This call will create a prototyped function is_everything(...) in the calling
54             package, with C positional parameters and an optional human-readable
55             message. (Think C, C).
56              
57             =head1 FUNCTIONS
58              
59             All functions are exportable.
60              
61             =cut
62              
63 63     63   450 use Carp;
  63         170  
  63         3893  
64 63     63   39712 use Data::Dumper;
  63         438456  
  63         4394  
65 63     63   555 use Scalar::Util qw(weaken blessed set_prototype looks_like_number refaddr);
  63         131  
  63         4167  
66 63     63   21993 use parent qw(Exporter);
  63         16101  
  63         378  
67             our @EXPORT = qw(build_refute current_contract to_scalar);
68              
69             # NOTE HACK
70             # If we're being loaded after Test::More, we're *likely* inside a test script
71             # This has to be re-done properly
72             # Cannot instantiate *here* because cyclic dependencies
73             # so wait until current_contract() is called
74             our $MORE_DETECTED = Test::Builder->can("new") ? 1 : 0;
75              
76             =head2 build_refute name => \&CODE, %options
77              
78             This function
79              
80             =over
81              
82             =item * accepts a subroutine reference that returns a false value on success
83             and a brief description of the discrepancy on failure
84             (e.g. C<"$got != $expected">);
85              
86             Note that this function does not need to know anything about the testing
87             environment it is in, it just cares about its arguments
88             (think I).
89              
90             =item * builds an exportable wrapper around it that would talk to
91             the most recent L instance;
92              
93             =item * adds a method with the same name to L
94             so that object-oriented and functional interfaces
95             are as close to each other as possible.
96              
97             =back
98              
99             As a side effect, Assert::Refute's internals are added to the caller's
100             C<@CARP_NOT> array so that carp/croak points to where the built function
101             is actually used.
102              
103             B One needs to use Exporter explicitly if either C
104             or C option is in use. This MAY change in the future.
105              
106             Options may include:
107              
108             =over
109              
110             =item * C => 1 - add function to @EXPORT
111             (Exporter still has to be used by target module explicitly).
112              
113             =item * C => 1 - add function to @EXPORT_OK (don't export by default).
114              
115             =item * C => 1 - don't generate a function at all, just add to
116             L's methods.
117              
118             =item * C => 1 - don't generate any code.
119             Instead, assume that user has already done that and just add a method
120             to L and a prototyped exportable wrapper.
121              
122             This may be useful to create refutations based on subcontract or such.
123              
124             B<[EXPERIMENTAL]>.
125              
126             =item * C => C - number of arguments.
127             This will generate a prototyped function
128             accepting C scalars + optional description.
129              
130             =item * C => 1 - create a list prototype instead.
131             Mutually exclusive with C.
132              
133             =item * C => 1 - create a block function.
134              
135             =item * C => 1 - skip prototype, function will have to be called
136             with parentheses.
137              
138             =back
139              
140             The name must not start with C, C, or C.
141             Also colliding with a previously defined name would case an exception.
142              
143             =cut
144              
145             my %Backend;
146             my %Carp_not;
147             my $trash_can = __PACKAGE__."::generated::For::Cover::To::See";
148             my %known;
149             $known{$_}++ for qw(args list block no_proto manual
150             export export_ok no_create);
151              
152             sub build_refute(@) { ## no critic # Moose-like DSL for the win!
153 1038     1038 1 72175 my ($name, $cond, %opt) = @_;
154              
155 1038         1728 my $class = "Assert::Refute::Report";
156              
157 1038 100       3553 if ($name =~ /^(get_|set_|do_)/) {
158 1         136 croak "build_refute(): fucntion name shall not start with get_, set_, or do_";
159             };
160 1037 100 66     7670 if (my $backend = ( $class->can($name) && ($Backend{$name} || $class )) ) {
161 2         556 croak "build_refute(): '$name' already registered by $backend";
162             };
163 1035         2715 my @extra = grep { !$known{$_} } keys %opt;
  2092         5081  
164 1035 50       2364 croak "build_refute(): unknown options: @extra"
165             if @extra;
166             croak "build_refute(): list and args options are mutually exclusive"
167 1035 50 66     2821 if $opt{list} and defined $opt{args};
168              
169 1035         6621 my @caller = caller(0);
170 1035   33     3969 my $target = $opt{target} || $caller[0];
171              
172 1035 50 33     3469 confess "Too bad (@caller)" if !$target or $target eq __PACKAGE__;
173              
174 1035   100     2469 my $nargs = $opt{args} || 0;
175 1035 100       2063 $nargs = 9**9**9 if $opt{list};
176              
177 1035 100       1879 $nargs++ if $opt{block};
178              
179             # TODO Add executability check if $block
180             my $method = $opt{manual} ? $cond : sub {
181 62     62   241 my $self = shift;
182 62 100       88 my $message; $message = pop unless @_ <= $nargs;
  62         221  
183              
184 62         211 return $self->refute( scalar $cond->(@_), $message );
185 1035 100       5563 };
186             my $wrapper = $opt{manual} ? sub {
187 1   33 1   10 return $cond->( $Assert::Refute::DRIVER || current_contract(), @_ );
188             } : sub {
189 117 100   117   2224 my $message; $message = pop unless @_ <= $nargs;
  117         350  
190             return (
191             # Ugly hack for speed in happy case
192 117   66     492 $Assert::Refute::DRIVER || current_contract()
193             )->refute( scalar $cond->(@_), $message );
194 1035 100       4808 };
195 1035 100 100     6236 if (!$opt{no_proto} and ($opt{block} || $opt{list} || defined $opt{args})) {
      66        
196 1034 100 100     3441 my $proto = $opt{list} ? '@' : '$' x ($opt{args} || 0);
197 1034 100       2072 $proto = "&$proto" if $opt{block};
198 1034 100       2090 $proto .= ';$' unless $opt{list};
199              
200             # '&' for set_proto to work on a scalar, not {CODE;}
201 1034         4632 &set_prototype( $wrapper, $proto );
202             };
203              
204 1035         4520 $Backend{$name} = "$target at $caller[1] line $caller[2]"; # just for the record
205 1035         2080 my $todo_carp_not = !$Carp_not{ $target }++;
206 1035         1530 my $todo_create = !$opt{no_create};
207 1035 100       2147 my $export = $opt{export} ? "EXPORT" : $opt{export_ok} ? "EXPORT_OK" : "";
    100          
208              
209             # Magic below, beware!
210 63     63   41916 no strict 'refs'; ## no critic # really need magic here
  63         190  
  63         6554  
211              
212             # set up method for OO interface
213 1035         1392 *{ $class."::$name" } = $method;
  1035         3435  
214              
215             # FIXME UGLY HACK - somehow it makes Devel::Cover see the code in report
216 1035         1424 *{ $trash_can."::$name" } = $cond;
  1035         9702  
217              
218 1035 50       3882 if ($todo_create) {
219 1035         3041 *{ $target."::$name" } = $wrapper;
  1035         4412  
220 1035 100       2262 push @{ $target."::".$export }, $name
  1034         3327  
221             if $export;
222             };
223 1035 100       2108 if ($todo_carp_not) {
224 63     63   493 no warnings 'once';
  63         175  
  63         25002  
225 78         184 push @{ $target."::CARP_NOT" }, "Assert::Refute::Contract", $class;
  78         2059  
226             };
227              
228             # magic ends here
229              
230 1035         4094 return 1;
231             };
232              
233             =head2 current_contract
234              
235             Returns a L object.
236             Dies if no contract is being executed at the time.
237              
238             =cut
239              
240             sub current_contract() { ## nocritic
241 80 100   80 1 4598 return $Assert::Refute::DRIVER if $Assert::Refute::DRIVER;
242              
243             # Would love to just die, but...
244 17 100       71 if ($MORE_DETECTED) {
245 16         7404 require Assert::Refute::Driver::More;
246 16         140 return $Assert::Refute::DRIVER = Assert::Refute::Driver::More->new;
247             };
248              
249 1         159 croak "Not currently testing anything";
250             };
251              
252             =head2 to_scalar
253              
254             Convert an arbitrary value into a human-readable string.
255              
256             to_scalar( $value )
257             to_scalar( $value, $depth )
258              
259             If $value is undefined and $depth is not given, returns C<'(undef)'>
260             (so that it's harder to confuse with a literal C<'undef'>).
261              
262             If $value is a scalar and $depth is not given, returns $value as is,
263             without quotes or anything.
264              
265             Otherwise returns L to depth $depth (or unlimited by default).
266              
267             One SHOULD NOT rely on exact format of returned data.
268              
269             =cut
270              
271             sub to_scalar {
272 298     298 1 1320 my ($data, $depth) = @_;
273              
274 298 100 100     1195 if (!ref $data and !defined $depth) {
275             # auto-explain
276 195 100       1141 return defined $data ? $data : '(undef)';
277             };
278              
279 103 100       222 $depth = 0 unless defined $depth;
280              
281 103         222 local $Data::Dumper::Indent = 0;
282 103         185 local $Data::Dumper::Sortkeys = 1;
283 103         177 local $Data::Dumper::Maxdepth = $depth;
284 103         144 local $Data::Dumper::Quotekeys = 0;
285 103         159 local $Data::Dumper::Useqq = 1;
286 103         285 my $str = Dumper($data);
287 103         5281 $str =~ s/^\$VAR1 *= *//;
288 103         400 $str =~ s/;\s*$//s;
289 103         630 return $str;
290             };
291              
292             =head1 LICENSE AND COPYRIGHT
293              
294             This module is part of L suite.
295              
296             Copyright 2017-2018 Konstantin S. Uvarin. C<< >>
297              
298             This program is free software; you can redistribute it and/or modify it
299             under the terms of the the Artistic License (2.0). You may obtain a
300             copy of the full license at:
301              
302             L
303              
304             =cut
305              
306             1;