File Coverage

blib/lib/SimpleMock/Mocks/Path/Tiny.pm
Criterion Covered Total %
statement 113 113 100.0
branch 42 44 95.4
condition n/a
subroutine 50 50 100.0
pod n/a
total 205 207 99.0


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