File Coverage

blib/lib/Test/Future/AsyncAwait/Awaitable.pm
Criterion Covered Total %
statement 78 93 83.8
branch 8 18 44.4
condition 1 9 11.1
subroutine 14 15 93.3
pod 1 1 100.0
total 102 136 75.0


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2020-2024 -- leonerd@leonerd.org.uk
5              
6             package Test::Future::AsyncAwait::Awaitable 0.71;
7              
8 1     1   249355 use v5.14;
  1         4  
9 1     1   7 use warnings;
  1         2  
  1         54  
10              
11 1     1   6 use Test2::V0;
  1         2  
  1         8  
12              
13 1     1   1582 use Exporter 'import';
  1         4  
  1         287  
14             our @EXPORT_OK = qw(
15             test_awaitable
16             );
17              
18             =head1 NAME
19              
20             C - conformance tests for awaitable role API
21              
22             =head1 SYNOPSIS
23              
24             =for highlighter language=perl
25              
26             use Test::More;
27             use Test::Future::AsyncAwait::Awaitable;
28              
29             use My::Future::Subclass;
30              
31             test_awaitable "My subclass of Future",
32             class => "My::Future::Subclass";
33              
34             done_testing;
35              
36             =head1 DESCRIPTION
37              
38             This module provides a single test function, which runs a suite of subtests to
39             check that a given class provides a useable implementation of the
40             L role. It runs tests that simulate various
41             ways in which L will try to use an instance of this class,
42             to check that the implementation is valid.
43              
44             =cut
45              
46             =head1 FUNCTIONS
47              
48             =cut
49              
50             =head2 test_awaitable
51              
52             test_awaitable( $title, %args );
53              
54             Runs the API conformance tests. C<$title> is printed in the test description
55             output so should be some human-friendly string.
56              
57             Takes the following named arguments:
58              
59             =over 4
60              
61             =item class => STRING
62              
63             Gives the name of the class. This is the class on which the C
64             and C methods will be invoked.
65              
66             =item new => CODE
67              
68             Optional. Gives a callback function to invoke to construct a new pending
69             instance; used by the tests to create pending instances that would be passed
70             into the C keyword. As this is not part of the API as such, the test
71             code does not rely on being able to directly perform it via the API.
72              
73             This argument is optional; if not provided the tests will simply try to invoke
74             the regular C constructor on the given class name. For most
75             implementations this should be sufficient.
76              
77             $f = $new->();
78              
79             =item cancel => CODE
80              
81             Optional. Gives a callback function to invoke to cancel a pending instance, if
82             the implementation provides cancellation semantics. If this callback is
83             provided then an extra subtest suite is run to check the API around
84             cancellation.
85              
86             $cancel->( $f );
87              
88             =item force => CODE
89              
90             Optional. Gives a callback function to invoke to wait for a promise to invoke
91             its on-ready callbacks. Some future-like implementations will run these
92             immediately when the future is marked as done or failed, and so this callback
93             will not be required. Other implementations will defer these invocations,
94             perhaps until the next tick of an event loop or similar. In the latter case,
95             these implementations should provide a way for the test to wait for this to
96             happen.
97              
98             $force->( $f );
99              
100             =back
101              
102             =cut
103              
104             my $FILE = __FILE__;
105              
106             my %FIXED_MODULE_VERSIONS = (
107             'Future::PP' => '0.50',
108             'Future::XS' => '0.09',
109             );
110              
111             sub _complain_package_version
112             {
113 0     0   0 my ( $pkg ) = @_;
114              
115             # Drill down to the most base class that isn't Future::_base
116             {
117 1     1   9 no strict 'refs';
  1         2  
  1         128  
  0         0  
118 0   0     0 $pkg = ${"${pkg}::ISA"}[0] while @{"${pkg}::ISA"} and ${"${pkg}::ISA"}[0] ne "Future::_base";
  0         0  
  0         0  
  0         0  
119             }
120              
121 1     1   7 my $pkgver = do { no strict 'refs'; ${"${pkg}::VERSION"} };
  1         2  
  1         1466  
  0         0  
  0         0  
  0         0  
122 0         0 my $wantver = $FIXED_MODULE_VERSIONS{$pkg};
123              
124 0 0 0     0 if( defined $wantver && $pkgver < $wantver ) {
125 0         0 diag( "$pkg VERSION is only $pkgver; this might be fixed by updating to version $wantver" );
126             }
127             else {
128 0         0 diag( "$pkg VERSION is $pkgver; maybe a later version fixes it?" );
129             }
130             }
131              
132             sub test_awaitable
133             {
134 1     1 1 304771 my ( $title, %args ) = @_;
135              
136 1         17 my $class = $args{class};
137 1   33 4   13 my $new = $args{new} || sub { return $class->new() };
  4         26  
138 1         4 my $cancel = $args{cancel};
139 1         3 my $force = $args{force};
140              
141             subtest "$title immediate done" => sub {
142 1     1   11000 ok( my $f = $class->AWAIT_NEW_DONE( "result" ), "AWAIT_NEW_DONE yields object" );
143              
144 1         648 ok( $f->AWAIT_IS_READY, 'AWAIT_IS_READY true' );
145 1         342 ok( !$f->AWAIT_IS_CANCELLED, 'AWAIT_IS_CANCELLED false' );
146              
147 1         345 is( [ $f->AWAIT_GET ], [ "result" ], 'AWAIT_GET in list context' );
148 1         1010 is( scalar $f->AWAIT_GET, "result", 'AWAIT_GET in scalar context' );
149 1         546 ok( defined eval { $f->AWAIT_GET; 1 }, 'AWAIT_GET in void context' );
  1         8  
  1         17  
150 1         15 };
151              
152             subtest "$title immediate fail" => sub {
153 1     1   442 ok( my $f = $class->AWAIT_NEW_FAIL( "Oopsie" ), "AWAIT_NEW_FAIL yields object" );
154              
155 1         450 ok( $f->AWAIT_IS_READY, 'AWAIT_IS_READY true' );
156 1         337 ok( !$f->AWAIT_IS_CANCELLED, 'AWAIT_IS_CANCELLED false' );
157              
158 1         350 my $LINE = __LINE__+1;
159 1         3 ok( !defined eval { $f->AWAIT_GET; 1 }, 'AWAIT_GET in void context' );
  1         6  
  0         0  
160 1 50       673 is( $@, "Oopsie at $FILE line $LINE.\n", 'AWAIT_GET throws exception' ) or
161             _complain_package_version( ref $f );
162 1         3882 };
163              
164 1 50       4982 my $fproto = $new->() or BAIL_OUT( "new did not yield an instance" );
165              
166             subtest "$title deferred done" => sub {
167 1     1   418 ok( my $f = $fproto->AWAIT_CLONE, 'AWAIT_CLONE yields object' );
168              
169 1         379 ok( !$f->AWAIT_IS_READY, 'AWAIT_IS_READY false' );
170              
171 1         333 $f->AWAIT_DONE( "Late result" );
172              
173 1         61 ok( $f->AWAIT_IS_READY, 'AWAIT_IS_READY true' );
174              
175 1         1906 is( scalar $f->AWAIT_GET, "Late result", 'AWAIT_GET in scalar context' );
176 1         22 };
177              
178             subtest "$title deferred fail" => sub {
179 1     1   413 ok( my $f = $fproto->AWAIT_CLONE, 'AWAIT_CLONE yields object' );
180              
181 1         379 ok( !$f->AWAIT_IS_READY, 'AWAIT_IS_READY false' );
182              
183 1         334 $f->AWAIT_FAIL( "Late oopsie" );
184              
185 1         60 ok( $f->AWAIT_IS_READY, 'AWAIT_IS_READY true' );
186              
187 1         1728 my $LINE = __LINE__+1;
188 1         4 ok( !defined eval { $f->AWAIT_GET; 1 }, 'AWAIT_GET in void context' );
  1         7  
  0         0  
189 1 50       656 is( $@, "Late oopsie at $FILE line $LINE.\n", 'AWAIT_GET throws exception' ) or
190             _complain_package_version( ref $f );
191 1         2922 };
192              
193             subtest "$title on-ready" => sub {
194 1 50   1   396 my $f = $new->() or BAIL_OUT( "new did not yield an instance" );
195              
196 1         12 my $called;
197 1         14 $f->AWAIT_ON_READY( sub { $called++ } );
  1         100  
198 1         28 ok( !$called, 'AWAIT_ON_READY CB not yet invoked' );
199              
200 1         354 $f->AWAIT_DONE( "ping" );
201 1 50       13 $force->( $f ) if $force;
202 1         4 ok( $called, 'AWAIT_ON_READY CB now invoked' );
203 1         3031 };
204              
205             $cancel and subtest "$title cancellation" => sub {
206 1 50   1   3116 my $f1 = $new->() or BAIL_OUT( "new did not yield an instance" );
207              
208 1         15 my $f2 = $f1->AWAIT_CLONE;
209              
210 1         49 $f1->AWAIT_CHAIN_CANCEL( $f2 );
211              
212 1         24 ok( !$f2->AWAIT_IS_CANCELLED, 'AWAIT_IS_CANCELLED false before cancellation' );
213              
214 1         360 $cancel->( $f1 );
215              
216 1         133 ok( $f2->AWAIT_IS_CANCELLED, 'AWAIT_IS_CANCELLED true after AWAIT_ON_CANCEL propagation' );
217              
218 1 50       360 my $f3 = $new->() or BAIL_OUT( "new did not yield an instance" );
219              
220 1         12 my $cancelled;
221 1         16 $f3->AWAIT_ON_CANCEL( sub { $cancelled++ } );
  1         21  
222              
223 1         14 $cancel->( $f3 );
224              
225 1         66 ok( $cancelled, 'AWAIT_ON_CANCEL invoked callback' );
226 1 50       1766 };
227             }
228              
229             =head1 AUTHOR
230              
231             Paul Evans
232              
233             =cut
234              
235             0x55AA;