summaryrefslogtreecommitdiff
path: root/lib/Test/Future.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Test/Future.pm')
-rw-r--r--lib/Test/Future.pm141
1 files changed, 141 insertions, 0 deletions
diff --git a/lib/Test/Future.pm b/lib/Test/Future.pm
new file mode 100644
index 0000000..f2a7d5f
--- /dev/null
+++ b/lib/Test/Future.pm
@@ -0,0 +1,141 @@
+# You may distribute under the terms of either the GNU General Public License
+# or the Artistic License (the same terms as Perl itself)
+#
+# (C) Paul Evans, 2014 -- leonerd@leonerd.org.uk
+
+package Test::Future;
+
+use strict;
+use warnings;
+use base qw( Test::Builder::Module );
+
+our $VERSION = '0.32';
+
+our @EXPORT = qw(
+ no_pending_futures
+);
+
+use Scalar::Util qw( refaddr );
+
+use constant HAVE_DEVEL_MAT_DUMPER => defined eval { require Devel::MAT::Dumper };
+
+=head1 NAME
+
+C<Test::Future> - unit test assertions for L<Future> instances
+
+=head1 SYNOPSIS
+
+ use Test::More tests => 2;
+ use Test::Future;
+
+ no_pending_futures {
+ my $f = some_function();
+
+ is( $f->get, "result", 'Result of the some_function()' );
+ } 'some_function() leaves no pending Futures';
+
+=head1 DESCRIPTION
+
+This module provides unit testing assertions that may be useful when testing
+code based on, or using L<Future> instances or subclasses.
+
+=cut
+
+=head1 FUNCTIONS
+
+=cut
+
+=head2 no_pending_futures( \&code, $name )
+
+Runs the given block of code, while keeping track of every C<Future> instance
+constructed while doing so. After the code has returned, each of these
+instances are inspected to check that they are not still pending. If they are
+all either ready (by success or failure) or cancelled, the test will pass. If
+any are still pending then the test fails.
+
+If L<Devel::MAT> is installed, it will be used to write a memory state dump
+after a failure. It will create a F<.pmat> file named the same as the unit
+test, but with the trailing F<.t> suffix replaced with F<-TEST.pmat> where
+C<TEST> is the number of the test that failed (in case there was more than
+one). A list of addresses of C<Future> instances that are still pending is
+also printed to assist in debugging the issue.
+
+It is not an error if the code does not construct any C<Future> instances at
+all. The block of code may contain other testing assertions; they will be run
+before the assertion by C<no_pending_futures> itself.
+
+=cut
+
+sub no_pending_futures(&@)
+{
+ my ( $code, $name ) = @_;
+
+ my @futures;
+
+ no warnings 'redefine';
+
+ my $new = Future->can( "new" );
+ local *Future::new = sub {
+ my $f = $new->(@_);
+ push @futures, $f;
+ $f->on_ready( sub {
+ my $f = shift;
+ for ( 0 .. $#futures ) {
+ refaddr( $futures[$_] ) == refaddr( $f ) or next;
+
+ splice @futures, $_, 1, ();
+ return;
+ }
+ });
+ return $f;
+ };
+
+ my $done = Future->can( "done" );
+ local *Future::done = sub {
+ my $f = $done->(@_);
+ pop @futures if !ref $_[0]; # class method
+ return $f;
+ };
+
+ my $fail = Future->can( "fail" );
+ local *Future::fail = sub {
+ my $f = $fail->(@_);
+ pop @futures if !ref $_[0]; # class method
+ return $f;
+ };
+
+ my $tb = __PACKAGE__->builder;
+
+ $code->();
+
+ my @pending = grep { !$_->is_ready } @futures;
+
+ return $tb->ok( 1, $name ) if !@pending;
+
+ my $ok = $tb->ok( 0, $name );
+
+ $tb->diag( "The following Futures are still pending:" );
+ $tb->diag( join ", ", map { sprintf "0x%x", refaddr $_ } @pending );
+
+ if( HAVE_DEVEL_MAT_DUMPER ) {
+ my $file = $0;
+ my $num = $tb->current_test;
+
+ # Trim the .t off first then append -$num.pmat, in case $0 wasn't a .t file
+ $file =~ s/\.(?:t|pm|pl)$//;
+ $file .= "-$num.pmat";
+
+ $tb->diag( "Writing heap dump to $file" );
+ Devel::MAT::Dumper::dump( $file );
+ }
+
+ return $ok;
+}
+
+=head1 AUTHOR
+
+Paul Evans <leonerd@leonerd.org.uk>
+
+=cut
+
+0x55AA;