Assert-Refute

 view release on metacpan or  search on metacpan

lib/Assert/Refute.pm  view on Meta::CPAN

            %conf = (%conf, %$_);
            $need_conf++;
        } elsif (!ref $_ and $_ eq '{}') {
            # TODO 0.15 remove together with auto-carp
            $need_conf++; # allow for -MAssert::Refute={}
        } elsif (!ref $_) {
            push @exp, $_;
        } else {
            croak "Unexpected argument in Assert::Refute->import: ".ref $_;
        };
    };

    $class->configure( \%conf, scalar caller ) if $need_conf;
    $class->export_to_level(1, undef, @exp);
};

my %known_callback = (
    skip => '',
    carp => sub {
        my $report = shift;
        warn $report->get_tap . _report_mess( $report );
    },
    croak => sub {
        my $report = shift;
        die $report->get_tap . _report_mess( $report );
    },
);

# TODO maybe public method in Report?
sub _report_mess {
    my $report = shift;

    my $state = $report->is_passing ? "passed" : "failed";
    my $title = $report->get_title;
    my $str   = $title ? "Contract '$title' $state" : "Contract $state";
    return Carp::shortmess($str);
};

my %default_conf = (
    on_fail => 'carp',
    on_pass => 'skip',
);

=head2 try_refute { ... }

Check whether given contract BLOCK containing zero or more assertions passes.

Contract will fail if any of the assertions fails,
a C<plan> is declared and not fulfilled,
or an exception is thrown.
Otherwise it is assumed to pass.

The BLOCK must accept one argument, the contract execution report,
likely a L<Assert::Refute::Report> instance.

More arguments MAY be added in the future.
Return value is ignored.

A read-only report instance is returned by C<try_refute> instead.

If C<on_pass>/C<on_fail> callbacks were specified during C<use> or
using C<configure>, they will also be executed if appropriate.

If C<NDEBUG> or C<PERL_NDEBUG> environment variable is set at compile time,
this block is replaced with a stub
which returns an unconditionally passing report.

This is basically what one expects from a module in C<Assert::*> namespace.

=cut

sub try_refute(&;@) { ## no critic # need prototype
    my ( $block, @arg ) = @_;

    # Should a missing config even happen? Ok, play defensively...
    my $conf = $CALLER_CONF{+caller};
    if( !$conf ) {
        $conf = __PACKAGE__->configure( {}, scalar caller );
    };
    return $conf->{skip_all} if exists $conf->{skip_all};

    my $report = $conf->{driver}->new;
    eval {
        $report->do_run($block);
        1;
    } || do {
        $report->done_testing(
            $@ || Carp::shortmess( 'Contract execution interrupted' ) );
    };

    # perform whatever action is needed
    my $callback = $conf->{ $report->is_passing ? "on_pass" : "on_fail" };
    $callback->($report) if $callback;

    return $report;
};

=head2 assert_refute { ... }

Check whether given contract BLOCK containing zero or more assertions passes.

Contract will fail if any of the assertions fail
or a C<plan> is declared and not fulfilled.
Otherwise it is assumed to pass.

Unlike with try_refute, exceptions are just let through.

The BLOCK must accept one argument, the contract execution report,
likely a L<Assert::Refute::Report> instance.

More arguments MAY be added in the future.
Return value is ignored.

A read-only report instance is returned by C<try_refute> instead.

If C<on_pass>/C<on_fail> callbacks were specified during C<use> or
using C<configure>, they will also be executed if appropriate.

If C<NDEBUG> or C<PERL_NDEBUG> environment variable is set at compile time,
this block is replaced with a stub
which returns an unconditionally passing report.

This is basically what one expects from a module in C<Assert::*> namespace.

B<[EXPERIMENTAL]>. Name and behavior MAY change in the future.
Should this function prove useful, it will become the successor
or C<try_refute>.

=cut

sub assert_refute(&;@) { ## no critic # need prototype
    unshift @_, '';

    goto &_real_assert;
};

=head2 refute_invariant "name" => sub { ... }

A named runtime assertion.

Exactly as above, except that a title is added to the report object
which will be appended to the emitted warning/error (if any).

Title can be queried via C<get_title> method in report object.

B<[EXPERIMENTAL]>. Name and meaning may change in the future.

=cut

sub refute_invariant(@) { ## no critic # need prototype
    my ( $name, $block, @arg ) = @_;

    croak q{Usage: refute_invariant "name" => sub { ... }"}
        unless $name and ref $block eq 'CODE';

    goto &_real_assert;
};

sub _real_assert {
    my ( $name, $block ) = @_;

    # Should a missing config even happen? Ok, play defensively...
    my $conf = $CALLER_CONF{+caller};
    if( !$conf ) {
        # TODO add configure_global & use default configuration
        $conf = __PACKAGE__->configure( { on_fail => 'carp' }, scalar caller );
    };
    return $conf->{skip_all} if exists $conf->{skip_all};

    my $report = $conf->{driver}->new->set_title($name)->do_run($block);

    # perform whatever action is needed
    my $callback = $conf->{ $report->is_passing ? "on_pass" : "on_fail" };
    $callback->($report) if $callback;

    return $report;
};

=head2 refute_and_report { ... }

Run a block of code with a fresh L<Assert::Refute::Report> object as argument.
Lock the report afterwards and return it.

For instance,

    my $report = refute_and_report {
        my $c = shift;
        $c->is( $price * $amount, $total, "Numbers add up" );
        $c->like( $header, qr/<h1>/, "Header as expected" );
        $c->can_ok( $duck, "quack" );
    };

Or alternatively one may resort to L<Test::More>-like DSL:

    use Assert::Refute qw(:all);
    my $report = refute_and_report {
        is      $price * $amount, $total, "Numbers add up";
        like    $header, qr/<h1>/, "Header as expected";
        can_ok  $duck, "quack";
    };

This method does not adhere C<NDEBUG>, apply callbacks, or handle expections.
It just executes the checks.
Not exported by default.

B<[EXPERIMENTAL]>. Name and behavior MAY change in the future.
Should this function prove useful, it will become the successor
or C<try_refute>.

=cut

sub refute_and_report (&;@) { ## no critic # need prototype
    my ( $block, @arg ) = @_;

    return Assert::Refute::Report->new->do_run($block);
};

=head2 contract { ... }

Save a contract BLOCK for future use:

    my $contract = contract {
        my ($foo, $bar) = @_;
        # conditions here
    };

    # much later
    my $report = $contract->apply( $real_foo, $real_bar );
    # Returns an Assert::Refute::Report with conditions applied

This is similar to how C<prepare> / C<execute> works in L<DBI>.

B<[DEPRECATED]> This function will disappear in v.0.20.

Prior to advent of C<try_refute>, this call used to be the main entry point
to this module.
This is no more the case, and a simple subroutine containing assertions
would fit in most places where C<contract> is appropriate.

Use L<Assert::Refute::Contract/contract> instead.

=cut

sub contract (&@) { ## no critic
    carp "contract{ ... } is DEPRECATED, use Assert::Refute::Contract::contract instead";

    require Assert::Refute::Contract;
    goto &Assert::Refute::Contract::contract;
};

=head2 plan tests => $n

Plan to run exactly C<n> assertions within a contract block.
Plan is optional, contract blocks can run fine without a plan.

A contract will fail unconditionally if plan is present and is not fulfilled.

C<plan> may only be called before executing any assertions.
C<plan> dies if called outside a contract block.

Not exported by default to avoid namespace pollution.

lib/Assert/Refute.pm  view on Meta::CPAN

=head2 contract_is

    contract_is $report, $signature, "Message";

Assert that a contract is fulfilled exactly to the specified extent.
See L<Assert::Refute::Report/get_sign> for signature format.

This may be useful for verifying assertions and contracts themselves.

This is actually a clone of L<Assert::Refute::T::Basic/contract_is>.

=cut

=head2 current_contract

Returns the L<Assert::Refute::Report> object being worked on.

If L<Test::Builder> has been detected and no contract block
is executed explicitly, returns a L<Assert::Refute::Driver::More> instance.
This allows to define assertions and run them uniformly under
both L<Assert::Refute> and L<Test::More> control.

Dies if no contract could be detected.

It is actually a clone of L<Assert::Refute::Build/current_contract>.

=head1 STATIC METHODS

Use these methods to configure Assert::Refute globally.

=head2 configure

    use Assert::Refute \%options;
    Assert::Refute->configure( \%options );
    Assert::Refute->configure( \%options, "My::Package");

Set per-caller configuration values for given package.
C<configure> is called implicitly by C<use Assert::Refute { ... }>
if hash parameter(s) are present.

%options may include:

=over

=item * on_pass - callback to execute if tests pass (default: C<skip>)

=item * on_fail - callback to execute if tests fail (default: C<carp>,
but not just C<Carp::carp> - see below).

=item * driver - use that class instead of L<Assert::Refute::Report>
as contract report.

=item * skip_all - reason for skipping ALL C<try_refute> blocks
in the affected package.
This defaults to C<PERL_NDEBUG> or C<NDEBUG> environment variable.

B<[EXPERIMENTAL]>. Name and meaning MAY change in the future.

=back

The callbacks MUST be either
a C<CODEREF> accepting L<Assert::Refute::Report> object,
or one of predefined strings:

=over

=item * skip - do nothing;

=item * carp - warn the stringified report;

=item * croak - die with stringified report as error message;

=back

Returns the resulting config (with default values added,etc).

As of current, this method only affects C<try_refute>.

=cut

my %conf_known;
$conf_known{$_}++ for qw( on_pass on_fail driver skip_all );

sub configure {
    my ($class, $given_conf, $caller) = @_;

    croak "Usage: $class->configure( \\%hash, \$target )"
        unless ref $given_conf eq 'HASH';

    my @extra = grep { !$conf_known{$_} } keys %$given_conf;
    croak "$class->configure: unknown parameters (@extra)"
        if @extra;

    # configure whoever called us by default
    $caller ||= scalar caller;

    my $conf = { %default_conf, %$given_conf };
    $conf->{on_fail} = _coerce_cb($conf->{on_fail});
    $conf->{on_pass} = _coerce_cb($conf->{on_pass});

    # Load driver
    if( $conf->{driver} ) {
        my $mod = "$conf->{driver}.pm";
        $mod =~ s#::#/#g;
        require $mod;
        croak "$conf->{driver} is not Assert::Refute::Report, cannot use as driver"
            unless $conf->{driver}->isa('Assert::Refute::Report');
    } else {
        $conf->{driver} = 'Assert::Refute::Report'; # this works for sure
    };

    if ($NDEBUG and !$conf->{skip_all}) {
        $conf->{skip_all} = "Assert::Refute turned off via NDEBUG=$NDEBUG";
    };

    if ($conf->{skip_all}) {
        my $default_report = $conf->{driver}->new;
        $default_report->plan( skip_all => $conf->{skip_all} );
        $default_report->done_testing;
        $conf->{skip_all} = $default_report;
    } else {



( run in 0.869 second using v1.01-cache-2.11-cpan-39bf76dae61 )