File Coverage

blib/lib/Assert/Refute/Contract.pm
Criterion Covered Total %
statement 56 58 96.5
branch 24 32 75.0
condition 6 8 75.0
subroutine 10 10 100.0
pod 4 4 100.0
total 100 112 89.2


line stmt bran cond sub pod time code
1             package Assert::Refute::Contract;
2              
3 17     17   216662 use 5.006;
  17         82  
4 17     17   86 use strict;
  17         37  
  17         391  
5 17     17   89 use warnings;
  17         35  
  17         756  
6             our $VERSION = '0.16';
7              
8             =head1 NAME
9              
10             Assert::Refute::Contract - Contract definition class for Assert::Refute suite
11              
12             =head1 DESCRIPTION
13              
14             This class represents a contract and is thus immutable.
15              
16             See L for its I to a specific case.
17              
18             =head1 SYNOPSIS
19              
20             use Assert::Refute::Contract;
21              
22             my $contract = Assert::Refute::Contract->new(
23             code => sub {
24             my ($c, $life) = @_;
25             $c->is( $life, 42 );
26             },
27             need_object => 1,
28             );
29              
30             # much later
31             my $result = $contract->apply( 137 );
32             $result->get_count; # 1
33             $result->is_passing; # 0
34             $result->get_tap; # Test::More-like summary
35              
36             =head1 DESCRIPTION
37              
38             This is a contract B class.
39             See L for execution log.
40             See L for convenient interface.
41              
42             =cut
43              
44 17     17   93 use Carp;
  17         71  
  17         1322  
45 17     17   1444 use parent qw(Exporter);
  17         909  
  17         100  
46              
47 17     17   6091 use Assert::Refute::Report;
  17         43  
  17         12584  
48              
49             our @CARP_NOT = qw(Assert::Refute Assert::Refute::Build);
50             our @EXPORT_OK = qw(contract);
51              
52             =head1 EXPORT
53              
54             C prototyped function is optionally exported.
55              
56             =head2 contract { ... }
57              
58             Save a contract BLOCK for future use:
59              
60             use Assert::Refute qw(:all);
61              
62             my $spec = contract {
63             my ($foo, $bar) = @_;
64             is $foo, 42, "Life";
65             like $bar, qr/b.*a.*r/, "Regex";
66             };
67              
68             # later
69             my $report = $spec->apply( 42, "bard" );
70             $report->get_count; # 2
71             $report->is_passing; # true
72             $report->get_tap; # printable summary *as if* it was Test::More
73              
74             The same may be written as
75              
76             my $spec = contract {
77             my ($report, @args) = @_;
78             $report->is( ... );
79             $report->like( ... );
80             } need_object => 1;
81              
82             The C form may be preferable if one doesn't want to pollute the
83             main namespace with test functions (C, C, C etc)
84             and instead intends to use object-oriented interface.
85              
86             Note that contract does B validate anything by itself,
87             it just creates a read-only L
88             object sitting there and waiting for an C call.
89              
90             The C call returns a L object containing
91             results of specific execution.
92              
93             This is similar to how C / C works in L.
94              
95             This function is equivalent to C (see below)
96             but may be more convenient in some cases.
97              
98             =cut
99              
100             sub contract (&@) { ## no critic
101 11 50   11 1 2726 croak "Odd number of elements in contract { ... } options"
102             unless @_ % 2;
103 11         38 my ($code, %opt) = @_;
104              
105 11         32 $opt{code} = $code;
106 11         74 return __PACKAGE__->new( %opt );
107             };
108              
109             =head1 OBJECT-ORIENTED INTERFACE
110              
111             =head2 new
112              
113             Assert::Refute::Contract->new( %options );
114              
115             %options may include:
116              
117             =over
118              
119             =item * C (required) - contract to be executed
120              
121             =item * C - if given, a contract execution object
122             will be prepended to C's argument list,
123             as if it was a method.
124              
125             This allows to run a contract without exporting anything to the calling
126             package.
127              
128             The name is not final, better suggestions wanted.
129              
130             =item * C = n or C = [min, max] - set limitation on
131             the number of accepted parameters.
132             Negative maximum value means unlimited.
133              
134             =back
135              
136             =cut
137              
138             my @new_required = qw( code );
139             my @new_essential = (@new_required, qw( need_object args ));
140             my @new_optional = qw( driver );
141              
142             my %new_arg;
143             $new_arg{$_}++ for @new_essential, @new_optional;
144              
145             my $def_driver = "Assert::Refute::Report";
146              
147             sub new {
148 24     24 1 263 my ($class, %opt) = @_;
149              
150 24         67 my @missing = grep { !$opt{$_} } @new_required;
  24         99  
151 24 50       87 croak( "Missing required arguments: @missing" )
152             if @missing;
153             croak( "'code' argument must be a subroutine" )
154 24 50       125 unless UNIVERSAL::isa($opt{code}, 'CODE');
155 24         76 my @extra = grep { !$new_arg{$_} } keys %opt;
  44         116  
156 24 50       92 croak( "Unknown options: @extra" )
157             if @extra;
158              
159 24 100       92 $opt{need_object} = $opt{need_object} ? 1 : 0;
160              
161             # argument count:
162             # * n means exactly n
163             # * (n, m) means from n to m
164             # * (n, -1) means from n to inf
165 24         50 my $args = $opt{args};
166 24 100       106 $args = [0, -1] unless defined $args; # == 0 is ok
167 24 100       97 $args = [ $args, $args ] unless ref $args eq 'ARRAY';
168 24 100       91 $args->[1] = 9**9**9 if $args->[1] < 0;
169 24 100       215 croak "Meaningless argument limits [$args->[0], $args->[1]]"
170             unless $args->[0] <= $args->[1];
171 23         53 $opt{args} = $args;
172              
173             # TODO validate driver
174 23   66     131 $opt{driver} ||= $def_driver;
175              
176 23         104 bless \%opt, $class;
177             };
178              
179             =head2 adjust( %overrides )
180              
181             Return a copy of this object with some overridden fields.
182              
183             The name is not perfect, better ideas wanted.
184              
185             %overrides may include:
186              
187             =over
188              
189             =item * driver - the class to perform tests.
190              
191             =back
192              
193             =cut
194              
195             sub adjust {
196 1     1 1 7 my ($self, %opt) = @_;
197              
198 1         3 my @dont = grep { $opt{$_} } @new_essential;
  3         5  
199 1 50       3 croak( "Attempt to override essential parameters @dont" )
200             if @dont;
201              
202 1 50       3 if (defined $opt{backend}) {
203             # TODO 0.20 kill it
204 0         0 carp( (ref $self)."->adjust: 'backend' is deprecated, use 'driver' instead");
205 0         0 $opt{driver} = delete $opt{backend};
206             };
207              
208 1         10 return (ref $self)->new( %$self, %opt );
209             };
210              
211             =head2 apply( @parameters )
212              
213             Spawn a new execution log object and run contract against it.
214              
215             Returns a locked L instance.
216              
217             =cut
218              
219             sub apply {
220 43     43 1 6118 my ($self, @args) = @_;
221              
222 43         139 my $c = $self->{driver};
223 43 50       214 $c = $c->new unless ref $c;
224             # TODO plan tests, argument check etc
225              
226             croak "contract->apply: expected from $self->{args}[0] to $self->{args}[1] parameters"
227 43 100 100     944 unless $self->{args}[0] <= @args and @args <= $self->{args}[1];
228              
229 38 100       126 unshift @args, $c if $self->{need_object};
230 38         80 local $Assert::Refute::DRIVER = $c;
231             eval {
232 38         121 $self->{code}->( @args );
233 34 50       129 $c->done_testing
234             unless $c->is_done;
235 34         92 1;
236 38 100       61 } || do {
237 4   50     72 $c->done_testing($@ || "Unexpected end of tests");
238             };
239              
240             # At this point, done_testing *has* been called unless of course
241             # it is broken and dies, in which case tests will fail.
242 38         145 return $c;
243             };
244              
245             =head1 LICENSE AND COPYRIGHT
246              
247             This module is part of L suite.
248              
249             Copyright 2017-2018 Konstantin S. Uvarin. C<< >>
250              
251             This program is free software; you can redistribute it and/or modify it
252             under the terms of the the Artistic License (2.0). You may obtain a
253             copy of the full license at:
254              
255             L
256              
257             =cut
258              
259             1; # End of Assert::Refute::Contract