Today I spent a few hours trying to nail down a nasty bug. Once I could replicate a small test case, what finally clued me in was an error message that looked impossible, but I remembered seeing it somewhere before. I just couldn't remember where. Consider this:
if ($c->user || $c->authenticate(undef, 'Pips Realm')) {
That was failing with "Undefined subroutine &Pips3::user called at ..."
Wait! What? I didn't call a subroutine. I called a method! What the hell is going on?
For our tests, we sometimes override the authenticated user in our Pips3 application. The core of this code looks like this:
sub new {
my ( $class, $user ) = @_;
unless (defined wantarray) {
croak('PIPTest::User->new($user) must not be called in void context');
}
my $self = bless {
pips_user => \&Pips3::user,
cat_user => \&Catalyst::Request::user,
user => $user,
} => $class;
$self->_set_user($user);
return $self;
}
sub _set_user {
my $self = shift;
# Get reference to sub returning the user object
my $coderef = $self->_return_user_coderef;
# Override Pips3::user
{
no warnings 'redefine';
*Pips3::user = $coderef;
*Catalyst::Request::user = $coderef;
}
# Return the user object for convenience
return $coderef->();
}
sub DESTROY {
my $self = shift;
no warnings 'redefine';
*Pips3::user = $self->{pips_user};
*Catalyst::Request::user = $self->{cat_user};
}
When you call 'new' with a valid user name, you get a token back and while it is in scope, you have overridden the user for the application. I was quite happy with this code, but there's a nasty, nasty bug in it. Once I remembered what it was, the fix was painfully obvious.
In Perl, when you try to pull a coderef out of a typeglob, you sometimes get some strange behavior:
sub Foo::bar {'foo'}
my $code1 = *Foo::bar{CODE};
my $code2 = *Foo::no_such_subroutine{CODE};
print "$code1 $code2";
__END__
# output is something like:
foo
CODE(0x8063d94) CODE(0x8063d70)
As you can see, you apparently get a code reference. If you try to call it, though, it will give the following error message "Undefined subroutine &main::no_such_subroutine called at ...". It even remembers the name of the non-existent subroutine! Even stranger:
#!/usr/bin/perl -l
sub Foo::bar {'foo'}
*Foo::baz = \&::baz;
print Foo->bar;
print Foo->baz;
__END__
foo
Undefined subroutine &main::baz called at test.pl line 7.
Wait! What? I didn't call a subroutine. I called a method! What the hell is going on?
Ooooooohhhhhhhh.
In Catalyst, when you use a plugin, your application gets the methods because it inherits them. It doesn't install them into your namespace. Thus, when Pips3 used the authentication plugin, my constructor above, trying to grab a reference to &Pips3::user, was clearly stupid. What's worse, when I tried to restore it, I had a non-existent method overriding an existing one! How's that for a blunder? The fix is simple:
sub new {
my ( $class, $user ) = @_;
unless (defined wantarray) {
croak('PIPTest::User->new($user) must not be called in void context');
}
my $self = bless {
pips_user => Pips3->can('user'),
cat_user => Catalyst::Request->can('user'),
user => $user,
} => $class;
$self->_set_user($user);
return $self;
}
This "strange behavior" is so that you can take a reference to an as-yet-undefined sub and call it later through the reference.