File Coverage

blib/lib/SimpleMock/Mocks/Path/Tiny.pm
Criterion Covered Total %
statement 111 111 100.0
branch 42 44 95.4
condition n/a
subroutine 49 49 100.0
pod n/a
total 202 204 99.0


line stmt bran cond sub pod time code
1             package SimpleMock::Mocks::Path::Tiny;
2 2     2   12 use strict;
  2         10  
  2         64  
3 2     2   8 use warnings;
  2         2  
  2         83  
4 2     2   6 use Carp qw(confess);
  2         3  
  2         130  
5 2     2   7 use Storable qw(dclone);
  2         3  
  2         128  
6              
7             our $VERSION = '0.01';
8              
9             require Path::Tiny;
10              
11 2     2   6 no warnings 'redefine';
  2         3  
  2         102  
12              
13             # copied over for convenieince
14             use constant {
15 2         3056 PATH => 0,
16             CANON => 1,
17             VOL => 2,
18             DIR => 3,
19             FILE => 4,
20             TEMP => 5,
21             IS_WIN32 => ( $^O eq 'MSWin32' ),
22 2     2   8 };
  2         3  
23             my %formats = (
24             'ls' => [ 1024, log(1024), [ "", map { " $_" } qw/K M G T/ ] ],
25             'iec' => [ 1024, log(1024), [ "", map { " $_" } qw/KiB MiB GiB TiB/ ] ],
26             'si' => [ 1000, log(1000), [ "", map { " $_" } qw/kB MB GB TB/ ] ],
27             );
28              
29              
30             sub _get_path_mock {
31 61     61   74 my $path = shift;
32 61         95 for my $layer (reverse @SimpleMock::MOCK_STACK) {
33 61 100       306 return $layer->{PATH_TINY}{$path} if exists $layer->{PATH_TINY}{$path};
34             }
35 2         32 return undef;
36             }
37              
38             my $orig_path = \&Path::Tiny::path;
39              
40             *Path::Tiny::path = sub {
41 19     19   4105 my @arg = @_;
42 19 100       39 confess "No mock defined for path $_[0]" unless _get_path_mock($_[0]);
43 18         38 return $orig_path->(@arg);
44             };
45              
46             *Path::Tiny::slurp = sub {
47 3     3   985 return _get_path_mock($_[0])->{data};
48             };
49              
50             # synonyms for now. TBD if they need more customization
51 1     1   555 *Path::Tiny::slurp_raw = sub { &Path::Tiny::slurp; };
52 1     1   539 *Path::Tiny::slurp_utf8 = sub { &Path::Tiny::slurp; };
53              
54 3     3   1255 *Path::Tiny::spew = sub { shift };
55 1     1   679 *Path::Tiny::chmod = sub { 1 }; # no op chmod
56              
57             *Path::Tiny::cwd = sub {
58 2 100   2   538 $ENV{PATH_TINY_CWD} or die "You must set the env var PATH_TINY_CWD to mock the cwd call";
59 1         5 return Path::Tiny::_path($ENV{PATH_TINY_CWD});
60             };
61              
62 1     1   4 *Path::Tiny::append = sub { 1 };
63 1     1   4 *Path::Tiny::append_raw = sub { 1 };
64 1     1   4 *Path::Tiny::append_utf8 = sub { 1 };
65              
66             # assert defaults to true but can be overridden in mocks if needed
67             *Path::Tiny::assert = sub {
68 5     5   2215 my $self = shift;
69 5         12 my $assert = _get_path_mock($self->[0])->{assert};
70 5 100       9 if (defined $assert) {
71 3 100       15 $assert or Path::Tiny::Error->throw( "assert", $self->[0], "failed assertion" );
72             }
73 3         12 return $self;
74             };
75              
76             *Path::Tiny::children = sub {
77 4     4   974 my $self = shift;
78 4         6 my $path = $self->[0];
79             # must be defined as a file
80 4 100       10 $self->is_dir
81             or $self->_throw('opendir');
82              
83 3         4 my %all_paths;
84 3 50       5 $all_paths{$_} = 1 for map { keys %{ $_->{PATH_TINY} || {} } } @SimpleMock::MOCK_STACK;
  3         2  
  3         26  
85 3         9 my @mock_paths = keys %all_paths;
86              
87 3         5 my @children = grep { m|$path/[^/]*$| } @mock_paths;
  31         158  
88 3         7 return sort map { Path::Tiny::path($_) } @children;
  6         118  
89             };
90              
91             # copy the mock and all attributes to a new mock and return the path object of the new mock.
92             # there's a small hack here to keep the syntax light - if a path ends in /(?:^|\/)\w+/
93             # assume it's a directory
94             *Path::Tiny::copy = sub {
95 3     3   2166 my ($self, $dest_path) = @_;
96 3         7 my $source_path = $self->[0];
97              
98             # if target doesn't exist, assume it's a file
99             # if it does, see if the target is a directory
100 3         6 my $dest_mock = _get_path_mock($dest_path);
101             my $target_path = defined $dest_mock
102             ? $dest_mock->{data}
103 3 100       16 ? Path::Tiny::_path($dest_path)->[0]
    100          
104             : Path::Tiny::_path($dest_path, $self->basename)->[0]
105             : Path::Tiny::_path($dest_path)->[0];
106              
107             # now we have the copy, register it as a mock
108 3         119 SimpleMock::_register_into_current_scope(
109             PATH_TINY => {
110             $target_path => _get_path_mock($source_path),
111             },
112             );
113 3         681 my $copied = Path::Tiny::_path($target_path);
114             };
115              
116             *Path::Tiny::digest = sub {
117 2     2   735 my $self = shift;
118 2         4 my $path = $self->[0];
119             my $digest = _get_path_mock($path)->{digest}
120 2 100       5 or die "'digest' attribute must be defined for '$path' mock";
121 1         3 return $digest;
122             };
123              
124             # I think we can no-op these for now
125       1     *Path::Tiny::edit = sub {};
126       1     *Path::Tiny::edit_utf8 = sub {};
127       1     *Path::Tiny::edit_raw = sub {};
128       1     *Path::Tiny::edit_lines = sub {};
129       1     *Path::Tiny::edit_lines_utf8 = sub {};
130       1     *Path::Tiny::edit_lines_raw = sub {};
131              
132             *Path::Tiny::exists = sub {
133 2     2   3 my $self = shift;
134 2         5 my $exists = _get_path_mock($self->[0])->{exists};
135 2 100       8 return defined $exists
136             ? $exists
137             : 1;
138             };
139              
140             *Path::Tiny::is_file = sub {
141 2     2   3 my $path = $_[0]->[0];
142             return _get_path_mock($path)->{data}
143 2 100       4 ? 1 : 0;
144             };
145              
146             *Path::Tiny::is_dir = sub {
147 7     7   37 my $path = $_[0]->[0];
148             return _get_path_mock($path)->{data}
149 7 100       12 ? 0 : 1;
150             };
151              
152             # If there's a need, I guess I can spend time later on mocking filehandles
153 1     1   29 *Path::Tiny::filehandle = sub { die "Not implemented"; };
154              
155             # target file does not need to be mocked. This is just an attribute of the mock object
156             *Path::Tiny::has_same_bytes = sub {
157 2     2   4 my $path = $_[0]->[0];
158 2         5 return _get_path_mock($path)->{has_same_bytes};
159             };
160              
161             # not a full path iterator - only iterates through current directory
162             # (which I think should be fine for most unit tests)
163             *Path::Tiny::iterator = sub {
164 3     3   673 my ($self, $args) = @_;
165 3 100       17 $args->{recurse} and die "'recurse' is not supported on iterator()";
166 2         5 my @children = shift->children;
167             return sub {
168 5     5   17 shift @children;
169             }
170 2         40 };
171             *Path::Tiny::lines = sub {
172 5     5   2785 my $self = shift;
173 5         14 my $args = Path::Tiny::_get_args( shift, qw/binmode chomp count/ );
174 5         70 my $path = $self->[0];
175 5         12 my @lines = map { "$_\n" } split /\n/, _get_path_mock($path)->{data};
  15         27  
176 5 100       13 chomp(@lines) if $args->{chomp};
177 5         7 my $count = $args->{count};
178 5 100       11 my @ret = $count
179             ? splice(@lines, 0, $count)
180             : @lines;
181 5         15 return @ret;
182             };
183 1     1   237 *Path::Tiny::lines_raw = sub { &Path::Tiny::lines; };
184 1     1   1111 *Path::Tiny::lines_utf8 = sub { &Path::Tiny::lines; };
185              
186             # just succeed for now - can tweak later if use case exists
187 1     1   4 *Path::Tiny::mkdir = sub { shift };
188 1     1   51 *Path::Tiny::mkpath = sub { die "Deprecated functionality - not implemented" };
189              
190             # just succeed for now - can tweak later if use case exists
191             *Path::Tiny::move = sub {
192 2     2   1231 my ( $self, $dest ) = @_;
193 2 100       590 return -d $dest ? Path::Tiny::_path( $dest, $self->basename ) : Path::Tiny::_path($dest);
194             };
195 1     1   37 *Path::Tiny::realpath = sub { die "Not implemented"; };
196              
197             # just succeed for now
198 1     1   5 *Path::Tiny::remove = sub { 1 };
199 1     1   4 *Path::Tiny::remove_tree = sub { 1 };
200             *Path::Tiny::size= sub {
201 3     3   1103 my $path = $_[0]->[0];
202 3         6 return length(_get_path_mock($path)->{data});
203             };
204              
205             # note: not tested _human_size , but I think 't's OK
206             *Path::Tiny::size_human = sub {
207 3     3   1065 my $self = shift;
208 3         8 my $args = Path::Tiny::_get_args( shift, qw/format/ );
209 3 100       36 my $format = defined $args->{format} ? $args->{format} : "ls";
210 3 100       20 my $fmt_opts = $formats{$format}
211             or Carp::croak("Invalid format '$format' for size_human()");
212 2         5 my $size = $self->size;
213 2 50       7 return defined $size ? Path::Tiny::_human_size( $size, @$fmt_opts ) : "";
214             };
215              
216             # _formats only used in tests - ignore
217              
218             # hard code in data if needed
219             *Path::Tiny::stat = sub {
220 5     5   63 my $path = $_[0]->[0];
221 5         8 my $stat = _get_path_mock($path)->{stat};
222 5 100       16 defined $stat or die "stat must be defined in mock for $path";
223 4 100       16 ref $stat eq 'ARRAY' or die "stat muct be defined as an arrayref for $path";
224 3         9 return $stat;
225             };
226 1     1   3 *Path::Tiny::lstat = sub { &Path::Tiny::stat };
227              
228             # always true for now
229 1     1   7 *Path::Tiny::touch = sub { shift };
230 1     1   5 *Path::Tiny::touchpath = sub { shift };
231              
232             # visit should be fine, but not recursive not supported right now
233              
234             1;
235              
236             =head1 NAME
237              
238             SimpleMock::Mocks::Path::Tiny - mocks for testing Path::Tiny code
239              
240             =head1 DESCRIPTION
241              
242             This module overrides methods in Path::Tiny to allow you to unit test code with Path::Tiny in it.
243              
244             It currently doesn't mock everything, but covers a lot of use cases.
245              
246             I don't have any production code using this module, so I've written what I think are core mocks.
247             If you have a specific use case that could do with mocking, or spot issues that affect usage,
248             please implement via a pull request (or just request it and I'll implement when I have time)
249              
250             =head1 USAGE
251              
252             use SimpleMock qw(register_mocks);
253              
254             register_mocks(
255             PATH_TINY => {
256             '/path/to/dir/pr/file' => {
257              
258             # if data is set, it's implicitly a file, otherwise it's a directory
259             data => $file_content,
260              
261             # these are all true by default, but you can set to false for them to throw
262             # or return false as noted
263             assert => 0, # throws
264             exists => 0, # return 0
265             has_same_bytes => 0, # return 0 - value is hard coded for ALL comparisons on a mock
266              
267             # returns this hard coded value for the stat - set as appropriate (obviously fake below)
268             stat => [1,2,3,4],
269              
270             # digest hash for the mock. Set as appropriate if calling digest()
271             digest => '1a2b3c4d536f',
272             },
273             }
274             );
275              
276             For basic usage, you just need this:
277              
278             register_mocks(
279             PATH_TINY => {
280              
281             # file MUST have a data attribute
282             '/path/to/file.txt' => { data => 'file content' },
283              
284             # directory must NOT have a data attribute
285             '/path/to/dir' => {},
286             }
287             );
288              
289              
290             =head1 MOCK ATTRIBUTES
291              
292             These are all valid keys for mock attributes:
293              
294             =head2 data
295              
296             The data to return if the file was read via slurp or lines (and their raw and utf8 variants).
297             Setting this attribute implicitly defines the mock as a file, omitting it implies a directory.
298              
299             It is up to you to ensure utf8/raw values are set as expected.
300              
301             =head2 assert
302              
303             All calls on the mock to assert() return true by default. If you set this to 0, all calls to
304             assert throw instead. Hard coded on a per mock basis.
305              
306             =head2 exists
307              
308             All calls on the mock to exists() return true by default. If you set this to 0, it returns a false value.
309              
310             =head2 has_same_bytes
311              
312             All calls on the mock to has_same_bytes() return true by default. If you set this to 0, it returns a false value.
313              
314             =head2 stat
315              
316             Arrayref to return when calling stat. Example above is obviously fake, so amend to suit your tests needs as appropriate.
317              
318             This is also used for the return value of lstat.
319              
320             =head2 digest
321              
322             Hard coded digest value to return for all calls to digest() on the mock.
323              
324             =head2 size()
325              
326             This is set to the length of the data attribute.
327              
328             =head1 A few notes
329              
330             =head2 copy()
331              
332             If you are using copy, there's small differences in behavior between copy to file and copy to a directory
333              
334             my $p = path('/path/to/file.txt');
335              
336             # copy to an explicit file - no mock is needed for the target
337             $p->copy('/path/to/file.txt')
338              
339             # copy to a directory you must set the target dir as a mock
340             $p->copy('/path/to/dir');
341              
342             ie, in your `register` mocks call, you must have:
343              
344             '/path/to/dir' => {},
345              
346             so that the code knows that the path '/path/to/dir' is not a file.
347              
348             Note: if you explicitly set a mock for the target file, this will get overridden when making the copy.
349              
350             =head2 children()
351              
352             All children MUST be defined as mocks. Grep is used to retrieve immediate children in the mocks code.
353              
354             =head2 Not Windows friendly (`/` path seperator expected).
355              
356             Sorry. First stab at this, and I don't have a Windows system I can tweak and test this on.
357             If someone wants to add in functionality for it to work in Windows, please do.
358              
359             =head2 No recursion mocking
360              
361             `iterator` does NOT recurse child directories. If the flag is set, an exception is thrown.
362              
363             `visit` uses `iterator`, so an exception is thrown if you set the `recurse` argument.
364              
365             If recursion is really needed, it can be added, but so far it's not implemented.
366              
367             =cut