File Coverage

blib/lib/Module/Mask.pm
Criterion Covered Total %
statement 59 59 100.0
branch 12 12 100.0
condition 3 3 100.0
subroutine 19 19 100.0
pod 8 8 100.0
total 101 101 100.0


line stmt bran cond sub pod time code
1             package Module::Mask;
2              
3 4     4   901510 use strict;
  4         12  
  4         167  
4 4     4   108 use warnings;
  4         19  
  4         606  
5              
6             our $VERSION = '0.06';
7              
8             =head1 NAME
9              
10             Module::Mask - Pretend certain modules are not installed
11              
12             =head1 SYNOPSIS
13              
14             use Module::Mask;
15              
16             {
17             my $mask = new Module::Mask ('My::Module');
18             eval { require My::Module };
19             if ($@) {
20             # ... should be called
21             }
22             else {
23             warn "require succeeded unexpectedly"
24             }
25             }
26            
27             # The mask is out of scope, this should now work.
28             eval { require My::Module };
29              
30             # There's also an inverted version:
31             {
32             my $mask = new Module::Mask::Inverted qw( Foo Bar );
33              
34             # Now only Foo and Bar can be loaded by require:
35             eval {require Baz};
36             }
37              
38             =head1 DESCRIPTION
39              
40             Sometimes you need to test what happens when a given module is not installed.
41             This module provides a way of temporarily hiding installed modules from perl's
42             require mechanism. The Module::Mask object adds itself to @INC and blocks
43             require calls to restricted modules.
44              
45             Module::Mask will not affect modules already loaded at time of instantiation.
46              
47             =cut
48              
49 4     4   4442 use Module::Util qw( module_path );
  4         13464  
  4         365  
50 4     4   41 use Scalar::Util qw( weaken );
  4         9  
  4         480  
51 4     4   31 use Carp qw( shortmess );
  4         10  
  4         3502  
52              
53             # Don't want this to be loaded inside INC by calling shortmess
54             require Carp::Heavy;
55              
56             =head1 METHODS
57              
58             =head2 import
59              
60             use Module::Mask @modules;
61              
62             $class->import(@modules);
63              
64             Globally masks modules. This can be used to block optional modules for testing
65             purposes.
66              
67             perl -MModule::Mask=MyOptionalModule my_test.pl
68              
69             =cut
70              
71             sub import {
72 3     3   29 my $class = shift;
73 3         13 our $Mask = $class->new(@_);
74             }
75              
76             =head2 new
77              
78             $obj = $class->new(@modules);
79              
80             Returns a new instance of this class. Any arguments are passed to mask_modules.
81              
82             =cut
83              
84             sub new {
85 7     7 1 2869 my $class = shift;
86 7         24 my $self = bless {}, $class;
87              
88 7         37 $self->mask_modules(@_);
89 7         42 $self->set_mask;
90              
91 7         45 return $self;
92             }
93              
94             sub DESTROY {
95 4     4   4021 my $self = shift;
96              
97 4         39 $self->clear_mask();
98             }
99              
100             =head2 mask_modules
101              
102             $obj = $obj->mask_modules(@modules)
103              
104             Add the given modules to the mask. Arguments can be paths or module names,
105             module names will be stored internally as relative paths. So there's no
106             difference between the following statements:
107              
108             $mask->mask_modules('My::Module');
109             $mask->mask_modules('My/Module.pm');
110              
111             =cut
112              
113             sub mask_modules {
114 9     9 1 26 my ($self, @modules) = @_;
115              
116 9         26 for my $module (@modules) {
117 7 100       29 next unless defined $module;
118 6         40 $self->_mask_module($module);
119             }
120              
121 9         628 return $self;
122             }
123              
124             sub _path {
125 40     40   68 my ($self, $module) = @_;
126              
127 40   100     142 return module_path $module || $module;
128             }
129              
130             # internal method to allow overriding
131             sub _mask_module {
132 6     6   12 my ($self, $module) = @_;
133 6 100       27 my $path = $self->_path($module) or return;
134              
135 5         298 $self->{$path} = 1;
136             }
137              
138             =head2 clear_mask
139              
140             $obj = $obj->clear_mask()
141              
142             Stops the object from masking modules by removing it from @INC. This is
143             automatically called when object goes out of scope.
144              
145             =cut
146              
147             sub clear_mask {
148 11     11 1 16 my $self = shift;
149              
150 11 100       26 @INC = grep { !ref $_ or $_ != $self } @INC;
  124         418  
151              
152 11         211 return $self;
153             }
154              
155             =head2 set_mask
156              
157             $obj = $obj->set_mask()
158              
159             Makes the object start masking modules by adding it to @INC. This is called by
160             new().
161              
162             This also has the effect of moving the object to the front of @INC again, which
163             could prove useful if @INC has been manipulated since the object was first
164             instantiated.
165              
166             Calling this method on an object already in @INC won't cause multiple copies to
167             appear.
168              
169             =cut
170              
171             sub set_mask {
172 7     7 1 13 my $self = shift;
173              
174             # We might already be in @INC
175 7         30 $self->clear_mask;
176              
177 7         17 unshift @INC, $self;
178 7         37 weaken $INC[0]; # don't let @INC keep us alive.
179              
180 7         10 return $self;
181             }
182              
183             =head2 is_masked
184              
185             $bool = $obj->is_masked($module)
186              
187             Returns true if the mask object is currently masking the given module, false
188             otherwise.
189              
190             Module::Mask::Inverted objects have the opposite behaviour.
191              
192             =cut
193              
194             sub is_masked {
195 30     30 1 554 my ($self, $module) = @_;
196              
197 30         81 return $self->_is_listed($module);
198             }
199              
200             # internal method to determine whether a module is listed in the mask or not.
201             sub _is_listed {
202 34     34   63 my ($self, $module) = @_;
203 34 100       82 my $path = $self->_path($module) or return 0;
204              
205 33 100       1293 return $self->{$path} ? 1 : 0;
206             }
207              
208             @Module::Mask::Inverted::ISA = qw( Module::Mask );
209              
210             sub Module::Mask::Inverted::is_masked {
211 4     4   10 my $self = shift;
212              
213 4         13 return ! $self->_is_listed(@_);
214             }
215              
216             =head2 list_masked
217              
218             @modules = $obj->list_masked()
219              
220             Returns a list of modules that are being masked. These are in the form of relative file paths, as found in %INC.
221              
222             =cut
223              
224 1     1 1 6 sub list_masked { keys %{$_[0]} }
  1         10  
225              
226             =head2 INC
227              
228             Implements the hook in @INC. See perldoc -f require
229              
230             =cut
231              
232             # INC gets forced into main unless explicitly qualified
233             sub Module::Mask::INC {
234 30     30 1 502333 my ($self, $filename) = @_;
235              
236 30 100       111 if ($self->is_masked($filename)) {
237 7         32 die $self->message($filename);
238             }
239             else {
240 23         24383 return;
241             }
242             }
243              
244             =head2 message
245              
246             $message = $obj->message($filename)
247              
248             Returns the error message to be used when the filename is not found. This is
249             normally "$filename masked by $class", but can be overridden in subclasses if
250             necessary. Carp's L is used to make this message
251             appear to come from the caller, i.e. the C or C statement
252             attempting to load the file.
253              
254             One possible application of this would be to make the error message look more
255             like perl's native "Could not find $filename in \@INC ...".
256              
257             =cut
258              
259             sub message {
260 6     6 1 12 my ($self, $filename) = @_;
261 6         11 my $class = ref $self;
262              
263 6         1050 return shortmess("$filename masked by $class");
264             }
265              
266             1;
267              
268             __END__