#!/usr/bin/perl
use strict;
BEGIN {
$| = 1;
$^W = 1;
}
use Test::More tests => 56;
use Aspect;
# Lexicals to track call counts in the support class
my $new = 0;
my $foo = 0;
my $bar = 0;
my $inc = 0;
# Create the test object
my $object = My::One->new;
isa_ok( $object, 'My::One' );
is( $new, 1, '->new 1' );
######################################################################
# Basic Usage
# Do the methods act as normal
is( $object->foo, 'foo', 'foo not yet installed' );
is( $object->inc(2), 3, 'inc not yet installed' );
is( $foo, 1, '->foo is called' );
is( $inc, 1, '->inc is called' );
# Check that the null case does nothing
SCOPE: {
my $aspect = before {
# It's oh so quiet...
} call 'My::One::foo';
is( $object->foo, 'foo', 'Null case does not change anything' );
is( $foo, 2, '->foo is called' );
}
# ... and uninstalls properly
is( $object->foo, 'foo', 'foo uninstalled' );
is( $foo, 3, '->foo is called' );
# Check that return_value works as expected and does not pass through
SCOPE: {
my $aspect = before {
$_->return_value('bar')
} call "My::One::foo";
is( $object->foo, 'bar', 'before changing return_value' );
is( $foo, 3, '->foo is not called' );
}
# ... and uninstalls properly
is( $object->foo, 'foo', 'foo uninstalled' );
is( $foo, 4, '->foo is called' );
# Check that proceed works as expected and does not pass through
SCOPE: {
my $aspect = before {
$_->return_value;
} call "My::One::foo";
is( scalar($object->foo), undef, 'scalar process(0) shortcuts to undef' );
is_deeply( [ $object->foo ], [ ], 'list process(0) shortcuts to ()' );
is( $foo, 4, '->foo is not called' );
}
# ... and uninstalls properly
is( $object->foo, 'foo', 'foo uninstalled' );
is( $foo, 5, '->foo is called' );
# Check that params works as expected and does pass through
SCOPE: {
my $aspect = before {
my @p = $_->args;
splice @p, 1, 1, $p[1] + 1;
$_->args(@p);
} call qr/My::One::inc/;
is( $object->inc(2), 4, 'before advice changing params' );
is( $inc, 2, '->inc is called' );
}
# Check that we can rehook the same function.
# Check that we can run several simultaneous hooks.
SCOPE: {
my $aspect1 = before {
my @p = $_->args;
splice @p, 1, 1, $p[1] + 1;
$_->args(@p);
} call qr/My::One::inc/;
my $aspect2 = before {
my @p = $_->args;
splice @p, 1, 1, $p[1] + 1;
$_->args(@p);
} call qr/My::One::inc/;
my $aspect3 = before {
my @p = $_->args;
splice @p, 1, 1, $p[1] + 1;
$_->args(@p);
} call qr/My::One::inc/;
is( $object->inc(2), 6, 'before advice changing params' );
is( $inc, 3, '->inc is called' );
}
# Were the hooks removed cleanly?
is( $object->inc(3), 4, 'inc uninstalled' );
is( $inc, 4, '->inc is called' );
# Check the introduction of a permanent hook
before {
$_->return_value('forever');
} call 'My::One::inc';
is( $object->inc, 'forever', '->inc hooked forever' );
is( $inc, 4, '->inc not called' );
######################################################################
# Usage with Cflow
# Check before hook installation
is( $object->bar, 'foo', 'bar cflow not yet installed' );
is( $object->foo, 'foo', 'foo cflow not yet installed' );
is( $bar, 1, '->bar is called' );
is( $foo, 7, '->foo is called for both ->bar and ->foo' );
SCOPE: {
my $advice = before {
my $c = shift;
$c->return_value($c->my_key->self);
} call "My::One::foo"
& cflow my_key => "My::One::bar";
# ->foo is hooked when called via ->bar, but not directly
is( $object->bar, $object, 'foo cflow installed' );
is( $bar, 2, '->bar is called' );
is( $foo, 7, '->foo is not called' );
is( $object->foo, 'foo', 'foo called out of the cflow' );
is( $foo, 8, '->foo is called' );
}
# Confirm original behaviour on uninstallation
is( $object->bar, 'foo', 'bar cflow uninstalled' );
is( $object->foo, 'foo', 'foo cflow uninstalled' );
is( $bar, 3, '->bar is called' );
is( $foo, 10, '->foo is called for both' );
######################################################################
# Prototype Support
sub main::no_proto { shift }
sub main::with_proto ($) { shift }
# Control case
SCOPE: {
my $advice = before {
$_->return_value('wrapped')
} call 'main::no_proto';
is( main::no_proto('foo'), 'wrapped', 'No prototype' );
}
# Confirm correct parameter error before hooking
SCOPE: {
local $@;
eval 'main::with_proto(1, 2)';
like( $@, qr/Too many arguments/, 'prototypes are obeyed' );
}
# Confirm correct parameter error during hooking
SCOPE: {
my $advice = before {
$_->return_value('wrapped');
} call 'main::with_proto';
is( main::with_proto('foo'), 'wrapped', 'With prototype' );
local $@;
eval 'main::with_proto(1, 2)';
like( $@, qr/Too many arguments/, 'prototypes are obeyed' );
}
# Confirm correct parameter error after hooking
SCOPE: {
local $@;
eval 'main::with_proto(1, 2)';
like( $@, qr/Too many arguments/, 'prototypes are obeyed' );
}
######################################################################
# Caller Correctness
my @CALLER = ();
my $BEFORE = 0;
SCOPE: {
# Set up the Aspect
my $aspect = before { $BEFORE++ } call 'My::Three::bar';
isa_ok( $aspect, 'Aspect::Advice' );
isa_ok( $aspect, 'Aspect::Advice::Before' );
is( $BEFORE, 0, '$BEFORE is false' );
is( scalar(@CALLER), 0, '@CALLER is empty' );
# Call a method above the wrapped method
my $rv = My::Two->foo;
is( $rv, 'value', '->foo is ok' );
is( $BEFORE, 1, '$BEFORE is true' );
is( scalar(@CALLER), 2, '@CALLER is full' );
is( $CALLER[0]->[0], 'My::Two', 'First caller is My::Two' );
is( $CALLER[1]->[0], 'main', 'Second caller is main' );
}
SCOPE: {
package My::Two;
sub foo {
My::Three->bar;
}
package My::Three;
sub bar {
@CALLER = (
[ caller(0) ],
[ caller(1) ],
);
return 'value';
}
}
######################################################################
# Wantarray Support
my @CONTEXT = ();
# Before the aspects
SCOPE: {
() = Foo->before;
my $dummy = Foo->before;
Foo->before;
}
SCOPE: {
my $aspect = before {
if ( $_[0]->wantarray ) {
push @CONTEXT, 'ARRAY';
} elsif ( defined $_[0]->wantarray ) {
push @CONTEXT, 'SCALAR';
} else {
push @CONTEXT, 'VOID';
}
if ( wantarray ) {
push @CONTEXT, 'ARRAY';
} elsif ( defined wantarray ) {
push @CONTEXT, 'SCALAR';
} else {
push @CONTEXT, 'VOID';
}
} call 'Foo::before';
# During the aspects
() = Foo->before;
my $dummy = Foo->before;
Foo->before;
}
# After the aspects
SCOPE: {
() = Foo->before;
my $dummy = Foo->before;
Foo->before;
}
# Check the results in aggregate
is_deeply(
\@CONTEXT,
[ qw{
array
scalar
void
ARRAY VOID array
SCALAR VOID scalar
VOID VOID void
array
scalar
void
} ],
'All wantarray contexts worked as expected for before',
);
SCOPE: {
package Foo;
sub before {
if ( wantarray ) {
push @CONTEXT, 'array';
} elsif ( defined wantarray ) {
push @CONTEXT, 'scalar';
} else {
push @CONTEXT, 'void';
}
}
}
######################################################################
# Support Classes
package My::One;
sub new {
$new++;
bless {}, shift;
}
sub foo {
$foo++;
return 'foo';
}
sub bar {
$bar++;
return shift->foo;
}
sub inc {
$inc++;
return $_[1] + 1;
}