Monday, November 19, 2012

Testing with OpenSIPS' Embedded Perl Module

Courtesy of epicstreetmagazine.com*
The CommPeak development team enjoys tackling a variety of programming challenges, and is always eager to come up with different strategies that help make the development of the company's VoIP termination solutions (both front and back-end) the best possible.

The topics of the open-source SIP server and Perl scripts are old favorites, and we wanted to share these with you, along with some tips and explanations.



Perl is a powerful high-performance, flexible, free and open-source dynamic language used for system services, utilities, web programming and more.

OpenSIPS (an open-source SIP server) has a module for embedded Perl support that can be used to off-load some of the more complicated work to Perl, which allows us to solve various acute situations with its strong capabilities and arsenal of third-party modules (available on CPAN).

OpenSIPS even provides several higher-level Perl modules to be used from within Perl to gain access to OpenSIPS internals for easier interaction with your OpenSIPS configuration code.

One issue which Perl programmers stress is testing. Perl has a distinguished testing culture (it has invented the TAP testing protocol) and promotes testing as a way to provide Quality Assurance for your code (the interpreter itself has over 500K tests, for example).

However, OpenSIPS provides a problem for this, since none of their modules are on CPAN and they require the OpenSIPS application installed in order to use them. This means you cannot test any code you've written for OpenSIPS without having OpenSIPS installed.

Suppose you have a Perl module called "Users.pm" which is used to handle some user information. It makes use of OpenSIPS' high-level Perl module OpenSIPS.pm in order to interact with OpenSIPS internals. If you try to load it without having the OpenSIPS.pm module (for which you need the OpenSIPS application), your test code will fail.

# Users.pm
use strict;
use warnings;
use OpenSIPS;
use OpenSIPS::Message;

sub authenticate_user {
    my $sip_msg  = shift;
    my $from_hdr = $sip_msg->pseudoVar( '$(hdr(From))' );

    # do some work
    # ...

    return 1;
}

1;

As you can see, OpenSIPS will load the Users.pm module and call the authenticate_user function and send it an OpenSIPS::Message object. If you will try to load the Users.pm file from outside of OpenSIPS (especially where the OpenSIPS application is not installed), it will try to load OpenSIPS.pm and crash, because it will be available.

A good way around it would be to write any code you want to test in an additional module and load it each time from the Users.pm module file. The downside is that you will continue to load the file on each SIP connection. Even with the Linux Kernel file cache mechanism, this still seems like too much.

Another way to handle this is to make your test script fake the existence of OpenSIPS.pm, OpenSIPS::Messages, OpenSIPS::Constants, and any other module which your Users.pm script loads.

Faking the OpenSIPS::Message object

Preparing your own object is fairly simple:

# in your test script
package Fake::OpenSIPS::Message {
    sub new { my $class = shift; bless {@_}, $class }

    sub pseudoVar {
        my $self = shift;
        my $var  = shift;

        return $var eq '$(hdr(From))' ?
               $self->{'from'}        :
               undef;
    }
}

This allows you to create a fake object with fake pseudoVars and have a method for retrieving them as it would be done in the code:

use Users; # loads Users.pm 
 
# create a fake OpenSIPS::Message object:
my $msg = Fake::OpenSIPS::Message->new( from => 'name ' );
 
# call Users.pm's authentication function with a fake message object:
authenticate_user($msg);

Faking high-level Perl modules for OpenSIPS

The only problem with this approach is that Users.pm still loads OpenSIPS.pm which still might not exist on your dev machine where you run your tests:
 
$ perl users.t
Can't locate OpenSIPS.pm in @INC (@INC contains: [...])
BEGIN failed--compilation aborted at Users.pm [...]
Compilation failed in require at users.t [...]
BEGIN failed--compilation aborted at users.t [...]

We can get around that, though, with the flexibility of Perl.

Perl has the %INC hash, which indicates the loaded modules along with their locations. When trying to load a module, Perl checks for its existence in the hash. We can therefore mark modules which we don't want Perl to load by indicating they already exist in the %INC hash. It looks like this:

# override OpenSIPS
$INC{'OpenSIPS.pm'}           = 1;
$INC{'OpenSIPS/Message.pm'}   = 1;
$INC{'OpenSIPS/Constants.pm'} = 1;

The problem is that use statements are compile-time, which means they are loaded before your code is run. This means that those statements modifying the %INC hash will only run after trying to load OpenSIPS. Alas, this means that it will fail before it reaches our code for preventing the attempt to load and fail. Bummer.

However, Perl provides a manner of making code run at compile-time, even if it should be run-time. This can be done via compile-time code blocks. One such code block is the BEGIN block. This forces code to run in compile-time. If it's located before your use statements, it will run before them, thus paving the way to make the failing use statements an effective no-op, not failing and allowing our test to continue.

# override OpenSIPS
BEGIN {
    $INC{'OpenSIPS.pm'}           = 1;
    $INC{'OpenSIPS/Message.pm'}   = 1;
    $INC{'OpenSIPS/Constants.pm'} = 1;
}

Faking constants provided by OpenSIPS::Constants

Another possible remaining issue are constants provided by OpenSIPS::Constants, such as L_NOTICE that are useful in log output. This can be easily accomplished by providing a subroutine with the same name:

# override OpenSIPS' L_NOTICE
sub L_NOTICE {1}

Et Voila! You can write code that uses modules that do not exist without failing, while faking them to provide determined input in order to test your code.

*Image credits: EpicStreetMagazine

No comments:

Post a Comment