Beefy Boxes and Bandwidth Generously Provided by pair Networks
Your skill will accomplish
what the force of many cannot
 
PerlMonks  

Error: Attempt to reload module.. while testing failing require

by Discipulus (Abbot)
on Jan 20, 2019 at 18:53 UTC ( #1228758=perlquestion: print w/replies, xml ) Need Help??

Discipulus has asked for the wisdom of the Perl Monks concerning the following question:

Hello monks,

Ok, at the end I've manged to resolve on my own (deleting from %INC ), but I still have doubts.

I'm trying to test a different executable path to be used in a module and specified by setting an %ENV variable.

I supposed to use die inside a BEGIN block in the module if the specified executable is incorrect:

# Win32::Backup::Robocopy code BEGIN{ if( $ENV{PERL_ROBOCOPY_EXE} ){ my $robo = File::Spec->rel2abs( $ENV{PERL_ROBOCOPY_EXE} ); die "$robo does not exists!" unless -e $robo; die "$robo is not executable!" unless -x $robo; } }

It seemed ok to me on quick test: the module dies if rubbish is passed (probably the check must be improved.. suggestions?)

But when testing this feauture I discovered the module is present anyway in %INC even if died (the require returns true? how can I return 0 from the BEGIN block?).

So with this version of test ( update the same errors appears even without all BEGIN blocks):

#!perl use 5.010; use strict; use warnings; use Test::More; use Test::Exception; BEGIN # 1) this file should not exists { my $try = 'c:\robocopy.exe'; note("try using $try"); $ENV{PERL_ROBOCOPY_EXE} = $try; dies_ok { require Win32::Backup::Robocopy } 'expected to die with not existent executable'; #delete $INC{'Win32/Backup/Robocopy.pm'}; print "--->$_ \n" for grep{/Robocopy/} keys %INC; } BEGIN # 2) the HOSTS file exists in every win version, but is not exec +utable { my $try = -e 'C:\Windows\System32\drivers\etc\HOSTS' ? 'C:\Windows\System32\drivers\etc\HOSTS' : # systenative used if a 32bit perl. See # filesystem redirection oddities 'C:\Windows\Sysnative\drivers\etc\HOSTS'; note("try using $try"); $ENV{PERL_ROBOCOPY_EXE} = $try; dies_ok { require Win32::Backup::Robocopy } 'expected to die with not executable file'; #delete $INC{'Win32/Backup/Robocopy.pm'}; print "--->$_\n" for grep{/Robocopy/} keys %INC; } BEGIN # 3) this has to be the standard one { my $try = -e 'C:\Windows\System32\robocopy.exe' ? 'C:\Windows\System32\robocopy.exe' : 'C:\Windows\Sysnative\robocopy.exe'; note("try using $try"); $ENV{PERL_ROBOCOPY_EXE} = $try; #delete $INC{'Win32::Backup::Robocopy'}; ok (require Win32::Backup::Robocopy, 'default executable path'); } done_testing();

I got Attempt to reload Win32/Backup/Robocopy.pm aborted.

# try using c:\robocopy.exe ok 1 - expected to die with not existent executable --->Win32/Backup/Robocopy.pm # try using C:\Windows\System32\drivers\etc\HOSTS ok 2 - expected to die with not executable file --->Win32/Backup/Robocopy.pm # try using C:\Windows\System32\robocopy.exe Attempt to reload Win32/Backup/Robocopy.pm aborted. Compilation failed in require at .\t\01-ENV.t line 45. BEGIN failed--compilation aborted at .\t\01-ENV.t line 46.

Uncommenting the delete the test runs OK, but why is the module in %INC if the require died? There are better ways to do this?

L*

There are no rules, there are no thumbs..
Reinvent the wheel, then learn The Wheel; may be one day you reinvent one of THE WHEELS.

Replies are listed 'Best First'.
Re: Error: Attempt to reload module.. while testing failing require
by haukex (Chancellor) on Jan 20, 2019 at 22:23 UTC

    Note the pseudocode documented in require: If a module fails to compile (do returns undef), it will still get an %INC entry of undef, meaning that the entry still exists, but it indicates a failed load.

    $ echo "die" > Whatever.pm $ perl -wMstrict -MData::Dumper -I. -e 'eval {require Whatever}; print + Dumper({%INC{"Whatever.pm"}})' $VAR1 = { 'Whatever.pm' => undef };

    perldiag says about that error message: "Attempt to reload %s aborted. (F) You tried to load a file with use or require that failed to compile once already. Perl will not try to compile this file again unless you delete its entry from %INC. See require and %INC in perlvar." So deleteing the %INC entry is the way to go, if you want to structure your code this way.

    By the way, is the source of Win32::Backup::Robocopy this? I don't see it currently making use of $ENV{PERL_ROBOCOPY_EXE}. It would be good to see that source.

    how can I return 0 from the BEGIN block?

    I don't think you can... I'm not sure that you need BEGIN blocks in the test code in the first place (as you noted in your update).

    There are better ways to do this?

    My suggestion would be to not do the checks on $ENV{PERL_ROBOCOPY_EXE} in the main code of the module. It seems to be an OO module, so why not just check for robocopy.exe in new? If you want to give a default that should be modifiable globally, then you could put an our $DEFAULT_ROBOCOPY_EXE = length $ENV{PERL_ROBOCOPY_EXE} ? $ENV{PERL_ROBOCOPY_EXE} : 'C:\\Windows\\System32\\robocopy.exe'; in the main code of Win32::Backup::Robocopy, and then in new, do something along the lines of $self->{robocopy_exe} = $arg{robocopy_exe}//$DEFAULT_ROBOCOPY_EXE;, and then check for the existence of the file $self->{robocopy_exe}.

    If this were a non-OO module that has to do some initialization in the main code of the module, then I might abstract out that initialization into a sub, such as sub _init { ... } _init();, as this would allow me to call My::Module::_init() as many times as I want without having to reload the module.

      Thanks haukex for your crystalline suggestions,

      now I see: a module returning 0 will be automatically removed from %INC while a module that explicitly die will be not:

      echo die > Whatever.pm perl -wMstrict -MData::Dump -I. -e "my $ret;eval {$ret=require Whatev +er};dd $ret; dd %$INC{$_} for grep{/Whatever/} keys %INC;" undef # <--- require returned value ("Whatever.pm", undef) # die in the module will leaves the entry in % +INC echo 0 > Whatever.pm perl -wMstrict -MData::Dump -I. -e "my $ret;eval {$ret=require Whatev +er};dd $ret; dd %$INC{$_} for grep{/Whatever/} keys %INC;" undef # <--- require returned value

      So i have no chanche at all to make the require or use to fail (in the right, intended way, aka leaving %INC clean)?

      Yes you pointed to the right source of Win32::Backup::Robocopy (thanks!) but as I'm now git aware ( ;) the env branch was not online: now is there

      Definitevely put the check in the new is better: but I'd avoid the our solution: are not to avoid these in modules? In the new call I can put a _check_robocopy but definetively I have to die or croack if what is set in %ENV is invalid.

      In the test I have to delete from %INC anyway..

      Thanks!

      L*

      There are no rules, there are no thumbs..
      Reinvent the wheel, then learn The Wheel; may be one day you reinvent one of THE WHEELS.
        So i have no chanche at all to make the require or use to fail (in the right, intended way, aka leaving %INC clean)?

        That depends on what you're testing. You can override require (e.g. BEGIN { *CORE::GLOBAL::require = sub { ... } }, newer versions of Perl required), point require at a different file that returns a false value, or, if you really wanted to, you could put a return; in Win32::Backup::Robocopy (because simplifying a lot, use is require is do is eval, and you can return from the latter two). However, I wouldn't recommend that, because having a bare return in the main code of module is IMO quite unusual, and in a normal Perl script would cause an "Can't return outside a subroutine" error. If what you are testing is the behavior of Win32::Backup::Robocopy, then I'd only consider two options realistic: it dieing, or it succeeding. To test that, yes, you will have to delete from %INC to get require to reload the module - but actually, I'd recommend the solutions I suggested above instead.

        the our solution: are not to avoid these in modules?

        I don't see why not? IMHO it's fine to use them in the way I showed, it allows setting a global default while still giving the user per-object control via a new argument. If a package variable was the only way to set a module's option, then yes, I'd agree that's not the best design choice, since users would have to use local or risk action-at-a-distance. But in some cases, some defaults really are global - just one example, in my File::Replace, I have a $File::Replace::DISABLE_CHMOD global, which can be set in the case that the system doesn't support chmod (but it can still be set on a per-object basis).

        definetively I have to die or croack if what is set in %ENV is invalid.

        Hm, I think I understand, you want to let the user know if what they set in %ENV is invalid? I think it's a design choice at which point you do that and whyhow. For example, if $ENV{PERL_ROBOCOPY_EXE} contains an invalid location, but a valid location is passed to new, is that really a fatal error? TIMTOWTDI, but I might consider just issuing a warning when Win32::Backup::Robocopy is loaded with a bad $ENV{PERL_ROBOCOPY_EXE} setting.

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://1228758]
Front-paged by Corion
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others romping around the Monastery: (4)
As of 2019-07-16 18:54 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found

    Notices?