On 1/30/07, David Golden <[EMAIL PROTECTED]> wrote:
On 1/30/07, Joshua ben Jore <[EMAIL PROTECTED]> wrote:
> Interestingly, this has caused me to wonder how well Test::Exception
> handles the corner cases where $@ is clobbered during the scope ending
> of eval{} and related. I've just filed a bug against it at
> http://rt.cpan.org/Ticket/Display.html?id=24678. The overall moral is
> when using eval{} you have to test the return value as well as [EMAIL 
PROTECTED] $@
> can be empty under errors which is kind of a bummer.

This might be better as a perlbug than a bug in Test::Exception as
eval in a DESTROY subroutine will clobber $@ at the end of scope in an
ordinary eval block, too.

Localizing $@ in DESTROY does the trick and perhaps Perl should do
that automatically.

There's a bug there, sure. It's widespread enough (like *every*
version of perl from back-when-whatever up to bleadperl) that it's
saner to get Test::Exception to acknowledge that and try to work
around it. It's relatively simple... and then I spent some time
actually accounting for everything and it got more complex. Blech.
Here's a recounting.

sub died {
   my $function = shift @_;

   local $SIG{__DIE__} = $SIG{__DIE__};
   my $died = 0;
   my $succeeded = defined eval {
       $function->();

       # Return true and trap die()
       $SIG{__DIE__} = sub { $died = 1 } };
   my $saved_error = $@;
   my $got_error = defined( blessed( $saved_error ) ) || $saved_error;

   return $died or not( $succeeded ) or $got_error;
}

Under normal circumstances a die in an eval makes it return undef and
$@ is set and $@ is normal. Yay.

Here's some pathologies that are possible and why I want to delegate
this job to a module (this is also why you don't want to do this stuff
by hand. Use a module):

If the eval ended with no errors but there was a die() during its
scope cleanup, then eval returned undef and $@ was empty anyway. I had
to have $SIG{__DIE__} trapped to see that an error occurred. It'd have
been completely hidden from me otherwise. Got to localize this outside
of the eval so it's still set during the scope cleanup.  $died will be
true, $got_error and $failed will be false.

If the eval died and there was another die() during its scope cleanup
then $@ contains only the original error and not the additional
errors. $got_error, $failed, and $died will be true - it just won't
contain the intermediate errors. I've no great ideas to store that
without stupid hackery to redefine die().

If the eval died and there was another eval with an unlocalized $@
during the outer eval's scope cleaup then the eval returns undef but
$@ is empty. $failed and $died will be true but $got_error will be
false.

Those combinations of $failed/$died/$got_error just show that you
can't check one thing to see if there was an error, not if you're
being paranoid.

If die() got an exception object then you've got to be careful that
nothing does any other unlocalized block evals. Copying $@ to a safe
location immediately protects against that.

If the exception object has overloading then just using it like a
boolean can cause it to become false and or clobber $@ so you have to
ask whether its an object instead. Only if it isn't an object can you
treat it like a boolean. Then you can't use ref() because that returns
all kinds of wrong values. At least blessed() gets there properly.
defined(blessed(...)) gets there *all* the way. Woot.

Much painfulness later, I think I really ought to look at the source
for Text::Exception and see if it's missing any cases.

Josh

Reply via email to