tags:

views:

92

answers:

2

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.

+9  A: 

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.

rafl
+3  A: 

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.

DVK
Note that `Test::Without::Module` also does the `require` hooking. It just happens to have some additional magic to make modules that were already loaded appear as if they had never been loaded, depending on how hard you look at them.
rafl
@rafl - OK, will need to re-read the POD/source... didn't notice that, thx! Also, does it make "require" die?
DVK
NOTE: the sample code above will FAIL in its purpose iff your code does any clever `@INC` manipulation after this sample is executed which pre-pends to `@INC` (before the custom subroutine you just inserted).
DVK
NOTE2: Also, you need to consider whether you want to propagate to child processes (via setting `$ENV{PERL5OPT}`) - as usual, the devil to REALLY solid code is in the details which is why, self-education aside, CPAN modules should usually be used for real usage - Devel::Hide does this for example :)
DVK
It doesn't `die` in `require`, if that's what you're asking, but it does cause `require` to raise an exception itself by providing it with something with a false return value.
rafl
@rafl - close enough for government work :)
DVK