The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!perl

use strict;
use warnings;

use Test::More tests => 24;
use Test::Exception;

use Class::Agreement;

my $file = __FILE__;

# precondition NAME, BLOCK
#
# Specify that the method NAME must meet the precondition as specified in
# BLOCK.

{
    my $line;
    {

        package Camel;
        use Class::Agreement;
        precondition simple => sub { $_[1] > 0 };
        $line = __LINE__ - 1;
        sub simple { }
    }

    lives_ok { Camel->simple(5) } "method simple success";
    dies_ok  { Camel->simple(-1) } "method simple failure";

    eval { Camel->simple(-1) };
    like $@, qr/line \s+ $line/x, "simple failure line number";
    like $@, qr/$file/x,          "simple failure filename";
}

# In BLOCK, the variable @_ will be the argument list of the method.

{

    package Camel;
    precondition argcheck => sub {
        Test::More::is_deeply(
            \@_,
            [ 'Camel', 1, 2, 3 ],
            "method simple arguments"
        );
    };

    sub argcheck { }
}

Camel->argcheck( 1, 2, 3 );

# Modifying @_ WILL NOT MODIFY the arguments passed to the method. (The first
# item of @_ will be the class name or object, as usual.)

{

    package Camel;
    precondition argmod => sub {
        @_ = ( 4, 5, 6 );
    };

    sub argmod {
        Test::More::is_deeply(
            \@_,
            [ 'Camel', 1, 2, 3 ],
            "no method argument modification"
        );
    }
}

Camel->argmod( 1, 2, 3 );

# With methods, if the the precondition fails (returns false),
# preconditions for the parent class will be checked.

{
    my $has_invoked_parent_pre = 0;
    my $has_invoked_child_pre  = 0;

    {

        package ClassA;
        use Class::Agreement;

        sub new { bless {}, shift }

        precondition pass => sub { ++$has_invoked_parent_pre; 1 };
        sub pass { }

        precondition fail => sub { ++$has_invoked_parent_pre; 0 };
        sub fail { }

        package ClassB;
        use base 'ClassA';
        use Class::Agreement;

        precondition pass => sub { ++$has_invoked_child_pre; 1 };
        sub pass { }

        precondition fail => sub { ++$has_invoked_child_pre; 0 };
        sub fail { }
    }

    eval { ClassB->pass };
    is $has_invoked_child_pre, 1, "child precondition invoked on success";
    is $has_invoked_parent_pre, 0,
        "parent precondition NOT invoked on success";

    eval { ClassB->fail };
    is $has_invoked_child_pre,  2, "child precondition invoked on failure";
    is $has_invoked_parent_pre, 1, "parent precondition invoked on failure";
}

# If the preconditions for both the child's method and the parent's method
# fail, the input to the method must have been invalid.

{
    my ( $parent_line, $child_line );

    {

        package ClassA;

        $parent_line = __LINE__ + 1;
        precondition m => sub { $_[1] > 0 };
        sub m { }

        package ClassB;

        $child_line = __LINE__ + 1;
        precondition m => sub { not $_[1] % 2 };
        sub m { }
    }

    {
        my $usage_line = __LINE__ + 1;
        eval { ClassB->m(-3) };
        like $@, qr/ ClassB::m .* client .* input /isx, "client input error";
        like $@, qr/line $usage_line/,                  "failure line number";
        like $@, qr/$file/x,                            "failure filename";
    }

    # If the precondition for the parent passes, there's a problem with the
    # hierarchy between the class and the parent class.

    eval { ClassB->m(3) };
    like $@, qr/ hierarchy .* between .* ClassA .* ClassB /isx,
        "hierarchy error";
    like $@, qr/line $parent_line \(the parent\)/,
        "failure line number parent";
    like $@, qr/line $child_line \(the child\)/, "failure line number child";
    like $@, qr/$file/x, "failure filename";

# Note that only the relationships between child and parent classes are checked
# -- this module won't traverse the complete ancestry of a class.
}

{
    my $third_line;
    {

        package ClassC;
        use base 'ClassB';
        use Class::Agreement;

        $third_line = __LINE__ + 1;
        precondition m => sub { not $_[1] % 3 };
        sub m { }
    }

    eval { ClassC->m(4) };
    like $@, qr/ hierarchy .* between .* ClassB .* ClassC /isx,
        "hierarchy only between parent and child";
    like $@, qr/line \s+ $third_line/x, "failure line number";
    like $@, qr/$file/x, "failure filename";
}

# You can use this keyword multiple times to declare multiple preconditions on
# the given method.

{

    package Camel;

    precondition multiple => sub { $_[1] > 0 };
    precondition multiple => sub { not $_[1] % 2 };

    sub multiple { }
}

lives_ok { Camel->multiple(4) } "method multiple success";
dies_ok  { Camel->multiple(-4) } "method multiple failure first";
dies_ok  { Camel->multiple(3) } "method multiple failure second";
dies_ok  { Camel->multiple(-3) } "method multiple failure both";