The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
use t::boilerplate;

use Test::More;
use Test::Requires { Moo => 1.002 };
use English      qw( -no_match_vars );
use Scalar::Util qw( blessed refaddr );
use Try::Tiny;

BEGIN {
   {  package MyException;

      use Moo;

      extends 'Unexpected';
      with    'Unexpected::TraitFor::ErrorLeader';
      with    'Unexpected::TraitFor::ExceptionClasses';

      my $class = __PACKAGE__;

      $class->add_exception( 'A' );
      $class->add_exception( 'B', [ 'A' ] );
      $class->add_exception( 'C', { error => 'Class C', parents => 'A' } );
      $class->add_exception( 'D', [ qw( A B ) ] );
      $class->add_exception( 'E', 'A' );

      $INC{ 'MyException.pm' } = __FILE__;
   }
}

sub EXCEPTION_CLASS { 'MyException' }

use Unexpected::Functions 'Unspecified';

sub _eval_error () { my $e = $EVAL_ERROR; $EVAL_ERROR = undef; return $e }

my $class = 'MyException'; my $e = _eval_error;

is $class->ignore->[ 0 ], 'Try::Tiny', 'No initial ignore class';

ok $class->ignore_class( 'IgnoreMe' ), 'Set ignore class';

is $class->ignore->[ 1 ], 'IgnoreMe', 'Get ignore class';

eval { $class->throw_on_error };

ok ! _eval_error, 'No throw without error';

eval { eval { die 'In a pit of fire' }; $class->throw_on_error };

like _eval_error, qr{ \QIn a pit of fire\E }mx , 'Throws on error';

eval { $class->throw( 'PracticeKill' ) }; $e = _eval_error;

can_ok $e, 'message';

like $e->message, qr{ PracticeKill }mx, 'Message contains known string';

is blessed $e, $class, 'Good class'; my $min_level = $e->level;

like $e, qr{ PracticeKill \s* \z   }mx, 'Throws error message';

is $e->class, 'Unexpected', 'Default error classification';

my $addr = refaddr $e;

is refaddr $e->caught(), $addr, 'Catches self';

is refaddr $class->caught( $e ), $addr, 'Catches own objects';

my $can_trace = $e =~ m{ \A main }mx ? 1 : 0;

$can_trace or $ENV{UNEXPECTED_SHOW_RAW_TRACE} = 1;

SKIP: {
   $can_trace or skip 'Stacktrace broken', 1;

   like $e, qr{ \A main \[ \d+ / $min_level \] }mx, 'Package and default level';
};

eval { $e->throw() }; $e = _eval_error;

is refaddr $e, $addr, 'Throws self';

eval { $class->throw( $e ) }; $e = _eval_error;

is refaddr $e, $addr, 'Throws own objects';

eval { $e->throw( 'Not allowed' ) }; $e = _eval_error;

like $e, qr{ object \s+ with \s+ arguments }mx, 'No throwing objects with args';

eval { Unexpected->clone }; $e = _eval_error;

like $e, qr{ \QClone is an object method\E }mx, 'Clone is an object method';

like $e->clone( { error => 'Mutated' } ), qr{ Mutated }mx, 'Clone mutates';

eval { $class->throw() }; $e = _eval_error;

like $e, qr{ Unknown \s+ error }mx, 'Default error string';

eval { $class->throw( error => sub { 'Test firing' } ) }; $e = _eval_error;

like $e, qr{ Test \s+ firing }mx, 'Derefernces coderef as error string';

eval { $class->throw( 'error', args => {} ) }; $e = _eval_error;

like $e, qr{ not \s+ pass \s+ type \s+ constraint }mx, 'Attribute type error';

eval { $class->throw( 'error', [] ) }; $e = _eval_error;

is $e->error, 'error', 'Constucts from string and arrayref';

eval { $class->throw( 'error', { args => [] } ) }; $e = _eval_error;

is $e->error, 'error', 'Constructs from string and hashref';

eval { $class->throw( class => 'Unspecified', args => [ 'Parameter' ] ) };

$e = _eval_error;

like $e, qr{ \Q'Parameter' not specified\E }mx, 'Error string from class';

eval { $class->throw( Unspecified, args => [ 'Parameter' ] ) };

$e = _eval_error;

like $e, qr{ \Q'Parameter' not specified\E }mx, 'Error string from coderef';

eval { $class->throw( Unspecified, [ 'Parameter' ] ) }; $e = _eval_error;

like $e, qr{ \Q'Parameter' not specified\E }mx,
   'Error string from coderef - args shortcut';

eval { $class->throw( Unspecified ) }; $e = _eval_error;

like $e, qr{ \Qnot specified\E }mx, 'Error string from coderef no args';

$e = $class->caught( $e, { leader => 'different' } );

is $e->leader, 'different', 'Constructs from self plus mutation';

my ($line1, $line2, $line3);

SKIP: {
   $can_trace or skip 'Stacktrace broken', 1;

   sub test_throw { $class->throw( 'PracticeKill' ) }; $line1 = __LINE__;

   sub test_throw1 { test_throw() }; $line2 = __LINE__;

   eval { test_throw1() }; $line3 = __LINE__; $e = _eval_error;

   my @lines = $e->stacktrace;

   like $e, qr{ \A main \[ $line2 / \d+ \] }mx, 'Package and line number';

   is $lines[ 0 ], "main::test_throw line ${line1}", 'Stactrace line 1';

   is $lines[ 1 ], "main::test_throw1 line ${line2}", 'Stactrace line 2';

   is $lines[ 2 ], "main line ${line3}", 'Stactrace line 3';

   @lines = $e->stacktrace( 1 );

   is $lines[ 0 ], "main::test_throw1 line ${line2}",
      'Stactrace can skip frames';

   my $lines = $e->stacktrace;

   like $lines, qr{ main::test_throw }mx, 'Stacktrace can return a scalar';
};

my $level = $min_level + 1;

sub test_throw2 { $class->throw( 'PracticeKill', level => $level ) };

sub test_throw3 { test_throw2() }

sub test_throw4 { test_throw3() }; $line1 = __LINE__;

eval { test_throw4() }; $e = _eval_error;

SKIP: {
   $can_trace or skip 'Stacktrace broken', 1;

   like $e, qr{ \A main \[ $line1 / $level \] }mx, 'Specific leader level';
};

$line1 = __LINE__; eval {
   $class->throw( args  => [ 'flap' ],
                  class => 'nonDefault',
                  error => 'cat: [_1] cannot open: [_2]', ) }; $e = _eval_error;

eval { $e->class}; $e = _eval_error;

like $e, qr{ 'nonDefault' \s+ does \s+ not \s+ exist }mx,
   'Non existant exception class';

eval { $class->add_exception() }; $e = _eval_error;

like $e, qr{ \QParameter 'exception class' not specified\E }mx,
   'Undefined exception class';

eval { $class->add_exception( 'F', 'Unknown' ) }; $e = _eval_error;

like $e, qr{ Unknown \s+ does \s+ not \s+ exist }mx,
   'Parent class does not exist';

eval { $class->add_exception( 'A', 'Unexpected' ) }; $e = _eval_error;

like $e, qr{ A \s+ already \s+ exists }mx,
   'Exception class already exists';

$line1 = __LINE__; eval {
   $class->throw( args  => [ 'flap' ],
                  class => 'A',
                  error => 'cat: [_1] cannot open: [_2]', ) }; $e = _eval_error;

is $e->class, 'A', 'Specific error classification';

SKIP: {
   $can_trace or skip 'Stacktrace broken', 1;

   like $e,
      qr{ main\[ $line1 / \d+ \]:\scat:\s'flap'\scannot\sopen: }mx,
      'Placeholer substitution - with quotes';
};

use Unexpected::Functions
   { exception_class => 'MyException' }, qw( A catch_class inflate_message );

is A()->(), 'A', 'Imports exception';

my $qstate = Unexpected::Functions->quote_bind_values();

is $qstate, 1, 'Default quoting state';

Unexpected::Functions->quote_bind_values( 0 );

SKIP: {
   $can_trace or skip 'Stacktrace broken', 1;

   like $e, qr{ main\[ $line1 / \d+ \]:\scat:\sflap\scannot\sopen: }mx,
      'Placeholer substitution - without quotes';
};

ok !$class->is_exception(), 'Exception predicate - undef';
ok $class->is_exception( 'E' ), 'Exception predicate - true';
ok !$class->is_exception( 'F' ), 'Exception predicate - false';

$line1 = __LINE__; eval {
   $class->throw( args  => [ 'flap' ],
                  class => 'D',
                  error => 'cat: [_1] cannot open: [_2]', ) }; $e = _eval_error;

is $e->class, 'D', 'Current exception classification';

is $e->previous_exception->class, 'A', 'Previous exception';

is $e->instance_of(), 0, 'Null class is false';

is $e->instance_of( 'A' ), 1, 'Inherits exception class';

is $e->instance_of( A ), 1, 'Inherits exception class - coderef';

is $e->instance_of( 'E' ), 0, 'Does not match exception class';

eval { $e->instance_of( 'nonExistant' ) }; $e = _eval_error;

like $e, qr{ nonExistant \s+ does \s+ not \s+ exist }mx,
   'Non existant exception class throws';

eval { $class->throw( error => 'PracticeKill', level => 99 )}; $e = _eval_error;

SKIP: {
   $can_trace or skip 'Stacktrace broken', 1;

   like $e, qr{ /1 }mx, 'Level greater than number of frames';
};

$class->ignore_class( 'main' );

eval { $class->throw( 'PracticeKill' ) }; $e = _eval_error;

is $e->leader, q(), 'No leader';

is "${e}", "PracticeKill\n", 'Stringifies';

my $v = try { $class->throw( class => 'C' ) } catch_class [ C => undef ];

is $v, undef, 'No catch class';

$v = try { $class->throw( class => 'C' ) } catch_class [ C => sub { 42 } ];

is $v, 42, 'Catch class';

$v = try { $class->throw( class => 'D', error => 'Must have an error' ) }
           catch_class [ B => sub { 42 } ];

is $v, 42, 'Catch class - instance_of';

eval { try { $class->throw( class => 'C' ) } catch_class [ D => sub { 42 } ]; };

$e = _eval_error;

is "${e}", "Class C\n", 'Catch class - default throws';

eval { try { die 'string' } catch_class [ C => sub { 42 } ]; };

$e = _eval_error;

like "${e}", qr{ \A string }mx, 'Catch class - ignores strings';

$v = try { die 'string' } catch_class [ ':str' => sub { 42 } ];

is $v, 42, 'Catch class - string exceptions';

$v = try { die [] } catch_class [ 'ARRAY' => sub { 42 } ];

is $v, 42, 'Catch class - references';

$v = try         { $class->throw( class => 'C' ) }
     catch_class [ Unexpected => sub { 42 } ];

is $v, 42, 'Catch class - real class names';

eval { try { die } catch_class []; };

$e = _eval_error;

like "${e}", qr{ \A Died }mx, 'Catch class - undefined keys';

$v = try { $class->throw( class => 'Unspecified' ) }
     catch_class [ undef, sub { 1 }, 'Unspecified' => sub { 1 } ];

is $v, 1, 'Catch class - undefined catch_class keys';

eval { catch_class []; };

$e = _eval_error;

like "${e}", qr{ \Qbare catch_class\E }mx, 'Catch class - bare catch_class';

done_testing;

# Local Variables:
# mode: perl
# tab-width: 3
# End: