Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I prevent Perl from using a module, for testing purposes?

Tags:

testing

perl

I am developing a suite of Perl scripts and modules that then get deployed on different machines and systems round our company. Some facilities are dependent on a particular module which may or may not be installed on different machines. I have used 'eval' to detect whether or not this module is available.

I've just had a fault report which came down to the fact that the user had not successfully installed the module on his machine (but didn't realise that he hadn't): but the bug in my code was that I did not, in this case, pass the error condition up to the top level, so it was getting lost, and the script was just silently failing to perform part of its function.

In order to investigate it, I disabled the particular module on my machine, and easily found and fixed the problem. But the only way I could think of to disable it, short of uninstalling it, was to rename the file (which I had to do via sudo, of course).

I am now running all of my tests with this module unavailable, and it has thrown up a few other places where I am not handling the situation properly.

But what I now want to do is to write some tests for this condition: but how can I sensibly make this module unavailable temporarily within an automatic test. I really don't want my tests using sudo to move modules out the way (I may be doing other things on the machine at the same time).

Does anybody know a way that I can tell Perl "Do not find this module, wherever I try to 'use' or 'require' it from, for testing purposes"?

I'm running Perl 5.10.0 (on Fedora 12), and using Test::More and TAP::Harness. Some of our installations are running Perl 5.8, so I am willing to use 5.10 features in testing, but not in the code itself.

like image 581
Colin Fine Avatar asked Oct 11 '10 15:10

Colin Fine


2 Answers

There's a couple of CPAN modules doing just that. The one I often use is Test::Without::Module. Another one would be Devel::Hide. Those two, and a few others whose names I can't quite remember right now, all work pretty much the same way, by hooking into perl's module loading through either @INC, or CORE::GLOBAL::require. The details of that are documented in perldoc -f require.

like image 184
rafl Avatar answered Sep 18 '22 12:09

rafl


As rafl said, there are modules to do that.

If you're interested in the mechanics of it as well (on top of achieveing the result), there are 2 ways this can be done:

  1. Remove the module from the namespace once it's been loaded. Test::Without::Module does this - take a look at source code for details.

  2. Prevent the module from being loaded in the first place. The easiest approach for this is to use the capability of Perl to have subroutines as part of @INC array which is used when loading modules via use/require. The theory underlying this can be founds in require's perldoc - search the text for word "hooks".

Subroutine references are the simplest case. When the inclusion system walks through @INC and encounters a subroutine, this subroutine gets called with two parameters, the first a reference to itself, and the second the name of the file to be included (e.g., "Foo/Bar.pm"). The subroutine should return either nothing or else a list of up to three values ...

... 2. A reference to a subroutine. If there is no filehandle (previous item), then this subroutine is expected to generate one line of source code per call, writing the line into $_ and returning 1, then returning 0 at end of file.

So what you do is write a sub that (only for a specified packages) returns empty code.

# The following code was not tested - for illustrative purposes only
# MUST be done in the BEGIN block at the very beginning of the test
# BEFORE any "use Module"; lines
push @INC, \&my_sub; 
my %prohibited_module_files = map { $_=> 1} ("Foo/Bar.pm", "x.pm");
                              # Ideally, translate module names into file names
sub empty_module_sub {
    $_ = "1;\n"; # Empty module
    return 0; # End of file
}
sub my_sub {
    my ($coderef, $filename) = @_; # $coderef is \&my_sub
    if ($prohibited_modules{$filename}) {
        print STDERR "NOT loading module $filename - prohibited!\n";
        # Optionally, die here to simulate not finding the module!!!
        # Otherwise, load empty package
        return (undef, \&empty_module_sub);
    }
    return undef; # Continue searching @INC for good modules.
}

A slightly simpler (but not as interesting or flexible) approach would rely on the fact that "require" semantics first checks $INC{$filename} and, if that key exists in %INC hash, considers module to be loaded; if the key is mapped to a true value, it considers the module to have been already correctly loaded, and false value, dies with a "compile failed" type of error. So you can achieve a result similar to the custom sub above bt inserting undef or 1 values under appropriate key (filename matching the module name) into %INC in the BEGIN block at the beginning of your code.

like image 39
DVK Avatar answered Sep 20 '22 12:09

DVK