Hack #42. Find and Report Module Bugs

Fix problems in CPAN modules.

In an ideal world, all software is fully tested and bug free. Of course that's rarely the case.

Using Perl modules offers many advantages, including more thoroughly validated routines, tested and optimized solutions, and the fact that someone has already done part of your job for you. Sometimes, though, you may find that the shiny module that does exactly what you need actually does something different than it should have.

Here's some code that creates a proxy object FooProxy. When you create an instance of this proxy object, it should behave just like an instance of the original Foo object, but FooProxy could modify specific behavior of the Foo object, perhaps to log method calls or check access [Hack #48], without altering the Foo package itself:

package FooProxy;

sub new
{ 
    my $class = shift;
    my $foo   = Foo->new( @_ );
    bless \\$foo, $class;
}

sub can
{ 
    my $self = shift;
    return $$self->can( @_ );
}

1;

Here's some code that instantiates a FooProxy object, and being paranoid, attempts to double-check that the created object looks just like a Foo object:

# Create a proxy object
my $proxy = FooProxy->new( );

# Make sure the proxy acts like a Foo
if ($proxy->isa('Foo'))
{ 
    print "Proxy is a Foo!\\n";
}
else
{ 
    die "Proxy isn't a Foo!";
}

When you run this script, you might notice a problem. When you call a Foo method on the $fooproxy object, the method complains that the object isn't Foo. What's going on?

Instead of diving straight into the debugger or throwing print statements throughout the code, step back and take a logical approach [Hack #53]. Here's the Foo definition:

package Foo;
use UNIVERSAL::isa;

sub new
{ 
    my $class = shift;
    bless \\my $foo, $class;
}

sub isa
{ 
    1;
}

1;

Foo uses the CPAN module UNIVERSAL::isa to protect itself against people calling the method UNIVERSAL::isa( ) as a function.[3] When someone calls UNIVERSAL::isa( $some_foo, 'Class' ), UNIVERSAL::isa should detect the isa( ) method of the Foo object, and call that. In this case, though, isa( ) is executing in the context of FooProxy. This looks like a problem with the UNIVERSAL::isa module; you should file a bug report!

Write a Test

Instead of just reporting the bug generically and leaving the author to diagnose, fix, and verify, give the author an excellent head start by writing a test. Taking it one step further, you can even add this test directly to the module's own collection of tests. After downloading and unpacking UNIVERSAL-isa-0.05.tar.gz, look for its t/ subdirectory. Each .t file in this directory represents a unit test with one to many subtests. Add a new test to the package by creating a new .t file. UNIVERSAL::isa, however, already includes a bugs.t file, so you can just add the new test there.

You could rewrite the example code and add it to bugs.t. Just don't forget to increment the test count appropriately, because you're adding tests:

# really delegates calls to Foo
{
    package FooProxy;

    sub new
    {
        my $class = shift;
        my $foo   = Foo->new( @_ );
        bless \\$foo, $class;
    }

    sub can
    {
        my $self = shift;
        return $$self->can( @_ );
    }
}

my $proxy = FooProxy->new( );
isa_ok( $proxy, 'Foo' );

Run the test and make sure it fails. If so, it's a good test; it demonstrates what you consider to be a real bug.

Running the test is usually as simple as:

$ prove -lv t/bugs.t
# test output here...

Submitting a bug report

Now you've done a lot of the work for the author. Not only have you narrowed the problem down to a particular module, you have produced a test case that he or she can include with the module to ensure that the bug gets fixed and stays fixed in future revisions. Instead of submitting a bug report that merely explains what you think the problem is (or just the symptom), you can provide an implemented test case that demonstrates the problem and will prove that the ultimate fix really works.

It's helpful to have the Perl community review your findings to confirm your analysis. Perl Monks (http://www.perlmonks.org/) is a free community for Perl programmers. Many of the best-known names in the Perl community—authors, instructors, and even language designers—frequent Perl Monks and dispense their wisdom freely. It's easy to be sure that you've found a legitimate bug, only to find out that you misunderstood the expected behavior. Further, you might get more useful feedback, such as a pointer that the module you're using is outdated, and there's a much better replacement, or that another module more closely meets your needs.

Once you have confirmation that this is a bug, submit your report to the CPAN Request Tracker at http://rt.cpan.org/.[4] This site provides a simple interface to submit bug reports to the appropriate package maintainer, and then check the status of the report. This site supports user accounts (including your existing PAUSE ID) that are useful for tracking your numerous bug reports, but you don't have to create an account. If you choose to continue without an account, you may specify an email address with the bug report, and you'll receive updates when the module maintainer updates your ticket.

On the site, first search for distributions. This will give you a form where you can enter the package distribution name, UNIVERSAL::isa, and find a list of active bugs against it. From here, you can report your new bug, assuming someone else hasn't already submitted it!

In the submission form, fill in the requested information. For the subject, please be specific and concise. Instead of UNIVERSAL::isa is broken, consider isa( ) reports incorrect package type (?). Choose an appropriate severity, and indicate the module version or versions in which you observed the defect. There's a box to describe in more detail what you observed and how this behavior differs from your expectation. Note the comments on the submission page that suggest other useful information to include.

There is also a place to attach a file. Along with the basic bug report, you can submit a patch to the module to add your test case. To create the patch, extract the package, creating a versioned directory with the pure downloaded form. Next, copy that package directory to another directory without the version number:

$ cp -r UNIVERSAL-isa-0.05 UNIVERSAL-isa
               

Make your changes (incorporate the test script) to the files in UNIVERSAL-isa, and then make a patch against the official release. First, in each package directory, do make clean to clean up any build-related files. Now, in the directory above both package directories, run diff with the unified and recursive flags, to make the file readable and to pick up all of the changed files:

$ diff -ur UNIVERSAL-isa-0.05 UNIVERSAL-isa > isa_misbehaving.patch
               

This command will produce a patch that, when applied to the files in UNIVERSAL-isa-0.05/, will reproduce the changes you made to the module's test file. Simply include this patch with your bug report, and you'll give the package maintainer a huge head start on fixing the problem.

Attach the patch you created, and submit the form.

Check your email or the site periodically for the status of your bug. Obviously, if there's a fix, you will want to grab the new version quickly, but you also need to see if the author has rejected your bug. If so, research the issue more to determine whether the issue is truly where you thought it was, or if you need to debug your own code further.

Hacking the Hack

You can do more than merely submitting a bug report. With a well-written test case in hand, it's not as daunting a task to fix the bug yourself. Along with the patch that adds your unit tests, you could even submit a patch against the entire package source. The package tests, including the one you added, will verify that the code change is correct, so the maintainer just has to review the changes and apply them.

Tip

As it turns out, the bug is that the particular version of UNIVERSAL::isa called the method UNIVERSAL::can( ) as a function, not a method. Oops.

Regardless of whether you provide a fix to the package maintainer, submitting a good bug report with effective unit tests adds value to CPAN for all its users.



[3] Foo defines its own isa( ), so you must call $some_foo->isa( ) instead.

[4] Of course, the author might prefer another means of reporting. Check the module's documentation to be sure.

Get Perl Hacks now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.