diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3943a2c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,4 @@ +language: perl +perl: + - "5.16" + - "5.14" diff --git a/Changes b/Changes new file mode 100644 index 0000000..a35da3b --- /dev/null +++ b/Changes @@ -0,0 +1,5 @@ +Revision history for Perl extension Try::Catch. + +0.01 Thu Dec 18 23:25:21 2012 + - original version; created by h2xs 1.23 with options + -X -n URI::Simple diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..65bc8ad --- /dev/null +++ b/MANIFEST @@ -0,0 +1,13 @@ +.travis.yml +Changes +lib/Try/Catch.pm +Makefile.PL +MANIFEST This list of files +README.md +t/basic.t +t/context.t +t/erroneous_usage.t +t/finally.t +t/given_when.t +t/global_destruction_forked.t +t/when.t diff --git a/Makefile.PL b/Makefile.PL new file mode 100644 index 0000000..fcb7ecd --- /dev/null +++ b/Makefile.PL @@ -0,0 +1,12 @@ +use 5.010001; +use ExtUtils::MakeMaker; +# See lib/ExtUtils/MakeMaker.pm for details of how to influence +# the contents of the Makefile that is written. +WriteMakefile( + NAME => 'Try::Catch', + VERSION_FROM => 'lib/Try/Catch.pm', # finds $VERSION + PREREQ_PM => {}, # none + ($] >= 5.005 ? ## Add these new keywords supported since 5.005 + (ABSTRACT_FROM => 'lib/Try/Catch.pm', # retrieve abstract from module + AUTHOR => 'Mamod A. Mehyar ') : ()) +); diff --git a/README.md b/README.md new file mode 100644 index 0000000..2d581b1 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +Try-Catch version 0.001 +======================== + +A Try::Tiny Copy with Speed in mind + +USAGE +===== + +Same as Try::Tiny diff --git a/bench/trycatch.pl b/bench/trycatch.pl new file mode 100644 index 0000000..236a95e --- /dev/null +++ b/bench/trycatch.pl @@ -0,0 +1,83 @@ +use strict; +use warnings; +use lib '../lib'; +use Try::Catch(); +use Try::Tiny(); + +use Benchmark qw(:all) ; + +##simple +cmpthese(100000, { + 'Try::Tiny' => sub { + Try::Tiny::try { + + } Try::Tiny::catch { + + }; + }, + 'Try::Catch' => sub { + Try::Catch::try { + + } Try::Catch::catch { + + }; + }, +}); + +##try dies +cmpthese(100000, { + 'Try::Tiny Dies' => sub { + my @t = Try::Tiny::try { + die "foo"; + } Try::Tiny::catch { + + }; + }, + 'Try::Catch Dies' => sub { + my @t = Try::Catch::try { + die "foo"; + } Try::Catch::catch { + + }; + }, +}); + +##try finally no die +cmpthese(100000, { + 'Try::Tiny Finally no die' => sub { + my @t = Try::Tiny::try { + + } Try::Tiny::finally { + + }; + }, + 'Try::Catch finally no die' => sub { + my @t = Try::Catch::try { + + } Try::Catch::finally { + + }; + }, +}); + +###try catch and finally blocks +cmpthese(100000, { + 'Try::Tiny with finally' => sub { + Try::Tiny::try { + die "foo"; + } Try::Tiny::catch { + + } Try::Tiny::finally { + + }; + }, + 'Try::Catch with finally' => sub { + Try::Catch::try { + die "foo"; + } Try::Catch::catch { + + } Try::Catch::finally { + + }; + } +}); diff --git a/lib/Try/Catch.pm b/lib/Try/Catch.pm new file mode 100644 index 0000000..abc3bee --- /dev/null +++ b/lib/Try/Catch.pm @@ -0,0 +1,87 @@ +package Try::Catch; +use strict; +use warnings; +use Carp; +use Data::Dumper; +$Carp::Internal{+__PACKAGE__}++; +use base 'Exporter'; +our @EXPORT = our @EXPORT_OK = qw(try catch finally); +our $VERSION = 0.001; + +my $finally; +my $catch; + +sub try(&;@) { + my $wantarray = wantarray; + ##copy then reset + #reset blocks and counter + my $catch_code = $catch; + my $finally_code = $finally; + $finally = undef; + $catch = undef; + my $code = shift; + my @ret; + my $prev_error = $@; + + my $fail = not eval { + $@ = $prev_error; + if (!defined $wantarray) { + $code->(); + } elsif (!$wantarray) { + $ret[0] = $code->(); + } else { + @ret = $code->(); + } + + return 1; + }; + + my @args = $fail ? ($@) : (); + $@ = $prev_error; + + if ($fail) { + if ($catch_code) { + local $_ = $args[0]; + for ($_){ + if (!defined $wantarray) { + $catch_code->(@args); + } elsif (!$wantarray) { + $ret[0] = $catch_code->(@args); + } else { + @ret = $catch_code->(@args); + } + last; ## seems to boost speed by 7% + } + } + } + + $finally_code->(@args) if $finally_code; + return $wantarray ? @ret : $ret[0]; +} + +sub catch(&;@) { + croak 'Useless bare catch()' unless wantarray; + croak 'One catch block allowed' if $catch; + croak 'Missing semicolon after catch block' if $_[1]; + $catch = $_[0]; + return; +} + +sub finally(&;@) { + croak 'Useless bare finally()' unless wantarray; + croak 'One finally block allowed' if $finally; + croak 'Missing semicolon after finally block ' if $_[1]; + $finally = $_[0]; + return; +} + +1; + +__END__ +=head1 NAME + +Try::Catch - A Try::Tiny copy with speed in mind + +=head1 USAGE + +Same as Try::Tiny diff --git a/t/basic.t b/t/basic.t new file mode 100644 index 0000000..53d3045 --- /dev/null +++ b/t/basic.t @@ -0,0 +1,158 @@ +use strict; +use warnings; + +use Test::More; + +use Try::Catch; + +sub _eval { + local $@; + local $Test::Builder::Level = $Test::Builder::Level + 2; + return ( scalar(eval { $_[0]->(); 1 }), $@ ); +} + +sub lives_ok (&$) { + my ( $code, $desc ) = @_; + local $Test::Builder::Level = $Test::Builder::Level + 1; + + my ( $ok, $error ) = _eval($code); + + ok($ok, $desc ); + + diag "error: $@" unless $ok; +} + + +sub throws_ok (&$$) { + my ( $code, $regex, $desc ) = @_; + local $Test::Builder::Level = $Test::Builder::Level + 1; + + my ( $ok, $error ) = _eval($code); + + if ( $ok ) { + fail($desc); + } else { + like($error || '', $regex, $desc ); + } +} + + +my $prev; + +lives_ok { + try { + die "foo"; + }; +} "basic try"; + + +throws_ok { + try { + die "foo"; + } catch { die $_ }; +} qr/foo/, "rethrow"; + + +{ + local $@ = "magic"; + is( try { 42 }, 42, "try block evaluated" ); + is( $@, "magic", '$@ untouched' ); +} + +{ + local $@ = "magic"; + is( try { die "foo" }, undef, "try block died" ); + is( $@, "magic", '$@ untouched' ); +} + +{ + local $@ = "magic"; + like( (try { die "foo" } catch { $_ }), qr/foo/, "catch block evaluated" ); + is( $@, "magic", '$@ untouched' ); +} +# +is( scalar(try { "foo", "bar", "gorch" }), "gorch", "scalar context try" ); +is_deeply( [ try {qw(foo bar gorch)} ], [qw(foo bar gorch)], "list context try" ); +# +is( scalar(try { die } catch { "foo", "bar", "gorch" }), "gorch", "scalar context catch" ); +is_deeply( [ try { die } catch {qw(foo bar gorch)} ], [qw(foo bar gorch)], "list context catch" ); + + +lives_ok { + try { + die "foo"; + } catch { + my $err = shift; + + try { + like $err, qr/foo/; + } catch { + fail("shouldn't happen"); + }; + + pass "got here"; + } +} "try in try catch block"; + +throws_ok { + try { + die "foo"; + } catch { + my $err = shift; + + try { } catch { }; + + die "rethrowing $err"; + } +} qr/rethrowing foo/, "rethrow with try in catch block"; + + +sub Evil::DESTROY { + eval { "oh noes" }; +} + +sub Evil::new { bless { }, $_[0] } + +{ + local $@ = "magic"; + local $_ = "other magic"; + + try { + my $object = Evil->new; + die "foo"; + } catch { + pass("catch invoked"); + local $TODO = "i don't think we can ever make this work sanely, maybe with SIG{__DIE__}" if $] < 5.014; + like($_, qr/foo/); + }; + + is( $@, "magic", '$@ untouched' ); + is( $_, "other magic", '$_ untouched' ); +} + + +{ + my ( $caught, $prev ); + + { + local $@; + + eval { die "bar\n" }; + + is( $@, "bar\n", 'previous value of $@' ); + + try { + die { + prev => $@, + } + } catch { + $caught = $_; + $prev = $@; + } + } + + is_deeply( $caught, { prev => "bar\n" }, 'previous value of $@ available for capture' ); + is( $prev, "bar\n", 'previous value of $@ also available in catch block' ); +} + +done_testing(); diff --git a/t/context.t b/t/context.t new file mode 100644 index 0000000..1a4b733 --- /dev/null +++ b/t/context.t @@ -0,0 +1,65 @@ +use strict; +use warnings; + +use Test::More; + +use Try::Catch; + +#plan tests => +# (4+1) * 2 # list/scalar with exception (try + catch + 2 x finally) + is_deeply +#+ 4 # void with exception +#+ (3+1) * 2 # list/scalar no exception (try + 2 x finally) + is_deeply +#+ 3 # void no exception +#; + +my $ctx_index = { + VOID => undef, + LIST => 1, + SCALAR => '', +}; +my ($ctx, $die); + +for (sort keys %$ctx_index) { + $ctx = $_; + for (0,1) { + $die = $_; + if ($ctx_index->{$ctx}) { + is_deeply( + [ run() ], + [ $die ? 'catch' : 'try' ], + ); + } + elsif (defined $ctx_index->{$ctx}) { + is_deeply( + [ scalar run() ], + [ $die ? 'catch' : 'try' ], + ); + } + else { + run(); + 1; + } + } +} + +sub run { + try { + is (wantarray, $ctx_index->{$ctx}, "Proper context $ctx in try{}"); + die if $die; + return 'try'; + } + catch { + is (wantarray, $ctx_index->{$ctx}, "Proper context $ctx in catch{}"); + return 'catch'; + } + finally { + SKIP: { + skip "DESTROY() not called in void context on perl $]", 1 + if $] < '5.008'; + is (wantarray, undef, "Proper VOID context in finally{} 1"); + } + return 'finally'; + }; +} + +done_testing(); diff --git a/t/erroneous_usage.t b/t/erroneous_usage.t new file mode 100644 index 0000000..3926f5b --- /dev/null +++ b/t/erroneous_usage.t @@ -0,0 +1,71 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Test::More tests => 6; + +use Try::Catch; + +sub _eval { + local $@; + local $Test::Builder::Level = $Test::Builder::Level + 2; + return ( scalar(eval { $_[0]->(); 1 }), $@ ); +} + +sub throws_ok (&$$) { + my ( $code, $regex, $desc ) = @_; + local $Test::Builder::Level = $Test::Builder::Level + 1; + + my ( $ok, $error ) = _eval($code); + + if ( $ok ) { + fail($desc); + } else { + like($error || '', $regex, $desc ); + } +} + +throws_ok { + try { 1 }; catch { 2 }; +} qr/\QUseless bare catch()/, 'Bare catch() detected'; + +throws_ok { + try { 1 }; finally { 2 }; +} qr/\QUseless bare finally()/, 'Bare finally() detected'; + +throws_ok { + try { 1 }; catch { 2 } finally { 2 }; +} qr/\QUseless bare catch()/, 'Bare catch()/finally() detected'; + +throws_ok { + try { 1 }; finally { 2 } catch { 2 }; +} qr/\QUseless bare finally()/, 'Bare finally()/catch() detected'; + + +#throws_ok { +# try { 1 } catch { 2 } catch { 3 } finally { 4 } finally { 5 } +#} qr/\QA try() may not be followed by multiple catch() blocks/, 'Multi-catch detected'; + + +sub foo { + try { 0 }; catch { 2 } +} + +throws_ok { + if (foo()) { + # ... + } +} qr/\QUseless bare catch/, + 'Bare catch at the end of a function call'; + +sub bar { + try { 0 }; finally { 2 } +} + +throws_ok { + if (bar()) { + # ... + } +} qr/\QUseless bare finally/, + 'Bare finally at the end of a function call'; diff --git a/t/finally.t b/t/finally.t new file mode 100644 index 0000000..171f17d --- /dev/null +++ b/t/finally.t @@ -0,0 +1,104 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use Test::More tests => 20; + +use Try::Catch; + +try { + my $a = 1+1; +} catch { + fail('Cannot go into catch block because we did not throw an exception') +} finally { + pass('Moved into finally from try'); +}; + +try { + die('Die'); +} catch { + ok($_ =~ /Die/, 'Error text as expected'); + pass('Into catch block as we died in try'); +} finally { + pass('Moved into finally from catch'); +}; + +try { + die('Die'); +} finally { + pass('Moved into finally from catch'); +} catch { + ok($_ =~ /Die/, 'Error text as expected'); +}; + +try { + die('Die'); +} finally { + pass('Moved into finally block when try throws an exception and we have no catch block'); +}; + +try { + die('Die'); +} finally { + pass('First finally clause run'); +}; + +try { + # do not die +} finally { + if (@_) { + fail("errors reported: @_"); + } else { + pass("no error reported") ; + } +}; + +try { + die("Die\n"); +} finally { + is_deeply(\@_, [ "Die\n" ], "finally got passed the exception"); +}; + +try { + try { + die "foo"; + } + catch { + die "bar"; + } + finally { + pass("finally called"); + }; +}; + +$_ = "foo"; +try { + is($_, "foo", "not localized in try"); +} +catch { +} +finally { + #is(scalar(@_), 0, "nothing in \@_ (finally)"); + is($_, "foo", "\$_ not localized (finally)"); +}; +is($_, "foo", "same afterwards"); + +$_ = "foo"; +try { + is($_, "foo", "not localized in try"); + die "bar\n"; +} +catch { + is($_[0], "bar\n", "error in \@_ (catch)"); + is($_, "bar\n", "error in \$_ (catch)"); +} +finally { + is(scalar(@_), 1, "error in \@_ (finally)"); + is($_[0], "bar\n", "error in \@_ (finally)"); + is($_, "foo", "\$_ not localized (finally)"); +}; +is($_, "foo", "same afterwards"); + + + +1; diff --git a/t/given_when.t b/t/given_when.t new file mode 100644 index 0000000..45ebcd0 --- /dev/null +++ b/t/given_when.t @@ -0,0 +1,40 @@ +#!/usr/bin/perl + +use strict; +use warnings; + +use Test::More; + +BEGIN { + plan skip_all => "Perl 5.10 is required" unless eval { require 5.010 }; + plan tests => 2; +} + +use Try::Catch; + +use 5.010; +no if $] >= 5.017011, warnings => 'experimental::smartmatch'; + +my ( $error, $topic ); + +given ("foo") { + when (qr/./) { + try { + die "blah\n"; + } catch { + $topic = $_; + $error = $_[0]; + } + }; +} + +is( $error, "blah\n", "error caught" ); + +{ + local $TODO = "perhaps a workaround can be found" + if $] < 5.017003; + is( $topic, $error, 'error is also in $_' ); +} + +# ex: set sw=4 et: + diff --git a/t/global_destruction_forked.t b/t/global_destruction_forked.t new file mode 100644 index 0000000..e4aa58a --- /dev/null +++ b/t/global_destruction_forked.t @@ -0,0 +1,57 @@ +use strict; +use warnings; +use Test::More tests => 3; +use Try::Catch; + +{ + package WithCatch; + use Try::Catch; + + sub DESTROY { + try {} + catch {}; + return; + } +} + +{ + package WithFinally; + use Try::Catch; + + sub DESTROY { + try {} + finally {}; + return; + } +} + +my $parent = $$; + +try { + my $pid = fork; + unless ($pid) { + my $o = bless {}, 'WithCatch'; + $SIG{__DIE__} = sub { + exit 1 + if $_[0] =~ /A try\(\) may not be followed by multiple catch\(\) blocks/; + exit 2; + }; + exit 0; + } + waitpid $pid, 0; + is $?, 0, 'nested try in cleanup after fork does not maintain outer catch block'; +} +catch {}; + +try { + my $pid = fork; + unless ($pid) { + my $o = bless {}, 'WithFinally'; + exit 0; + } + waitpid $pid, 0; + is $?, 0, 'nested try in cleanup after fork does not maintain outer finally block'; +} +finally { exit 1 if $parent != $$ }; + +pass("Didn't just exit"); diff --git a/t/when.t b/t/when.t new file mode 100644 index 0000000..a830746 --- /dev/null +++ b/t/when.t @@ -0,0 +1,35 @@ +#!/usr/bin/perl + +use strict; +use warnings; +use feature "switch"; +use Test::More; + +BEGIN { + plan skip_all => "Perl 5.10 required" unless eval { require 5.010; 1 }; + plan tests => 5; +} + +use Try::Catch; + +use 5.010; +no if $] >= 5.017011, warnings => 'experimental::smartmatch'; + +my ( $foo, $bar, $other ); + +$_ = "magic"; + +try { + die "foo"; +} catch { + like( $_, qr/foo/ ); + when (/bar/) { $bar++ }; + when (/foo/) { $foo++ }; + default { $other++ }; +}; + +is( $_, "magic", '$_ not clobbered' ); + +ok( !$bar, "bar didn't match" ); +ok( $foo, "foo matched" ); +ok( !$other, "fallback didn't match" );