File Coverage

blib/lib/Test/Directory.pm
Criterion Covered Total %
statement 174 174 100.0
branch 69 70 98.5
condition 15 15 100.0
subroutine 30 30 100.0
pod 19 19 100.0
total 307 308 99.6


line stmt bran cond sub pod time code
1             package Test::Directory;
2              
3 9     9   239870 use strict;
  9         20  
  9         355  
4 9     9   50 use warnings;
  9         17  
  9         292  
5              
6 9     9   51 use Carp;
  9         22  
  9         722  
7 9     9   52 use Fcntl;
  9         19  
  9         4436  
8 9     9   59 use File::Spec;
  9         18  
  9         249  
9 9     9   13047 use File::Temp;
  9         323242  
  9         779  
10 9     9   82 use Test::Builder::Module;
  9         20  
  9         145  
11              
12             our $VERSION = '0.041';
13             our @ISA = 'Test::Builder::Module';
14              
15             ##############################
16             # Constructor / Destructor
17             ##############################
18              
19             my $template = 'test-directory-tmp-XXXXX';
20              
21             sub new {
22 15     15 1 7698 my $class = shift;
23 15         30 my $dir = shift;
24 15         35 my %opts = @_;
25              
26 15 100       46 if (defined $dir) {
27 12         275 $dir = File::Spec->join(split '/', $dir);
28 12 100       118781 mkdir $dir or croak "Failed to create '$dir': $!";
29             } else {
30 3         32 $dir = File::Temp->newdir( $template, CLEANUP=>0, DIR=>'.' )->dirname;
31             };
32              
33 13         1707 my %self = (dir => $dir);
34 13         163 bless \%self, $class;
35             }
36              
37             sub DESTROY {
38 13     13   2562 $_[0]->clean;
39             }
40              
41             ##############################
42             # Utility Functions
43             ##############################
44              
45             sub name {
46 193     193 1 451 my ($self,$path) = @_;
47 193         515 my @path = split /\//, $path;
48 193         285 my $file = pop @path;
49 193 100       7932 return @path ? File::Spec->catfile(@path,$file) : $file;
50             };
51              
52             sub path {
53 156     156 1 5415 my ($self,$file) = @_;
54 156 100       836 return defined($file)?
55             File::Spec->catfile($self->{dir}, $self->name($file)):
56             $self->{dir};
57             };
58              
59              
60             sub touch {
61 7     7 1 982 my $self = shift;
62 7         24 foreach my $file (@_) {
63 9         42 my $path = $self->path($file);
64 9 100       1822 sysopen my($fh), $path, O_WRONLY|O_CREAT|O_EXCL
65             or croak "$path: $!";
66 8         313 $self->{files}{$file} = 1;
67             };
68             };
69              
70             sub create {
71 5     5 1 1026 my ($self, $file, %opt) = @_;
72 5         16 my $path = $self->path($file);
73            
74 5 100       369 sysopen my($fh), $path, O_WRONLY|O_CREAT|O_EXCL
75             or croak "$path: $!";
76            
77 3         11 $self->{files}{$file} = 1;
78            
79 3 100       10 if (defined $opt{content}) {
80 1         14 print $fh $opt{content};
81             };
82 3 100       12 if (defined $opt{time}) {
83 1         34 utime $opt{time}, $opt{time}, $path;
84             };
85 3         96 return $path;
86             }
87              
88             sub mkdir {
89 10     10 1 945 my ($self, $dir) = @_;
90 10         26 my $path = $self->path($dir);
91 10 100       1026 mkdir($path) or croak "$path: $!";
92 9         49 $self->{directories}{$dir} = 1;
93             }
94              
95             sub check_file {
96 15     15 1 2833 my ($self,$file) = @_;
97 15         108 my $rv;
98 15 100       44 if (-f $self->path($file)) {
99 10         33 $rv = $self->{files}{$file} = 1;
100             } else {
101 5         21 $rv = $self->{files}{$file} = 0;
102             }
103 15         95 return $rv;
104             }
105              
106             sub check_directory {
107 9     9 1 64 my ($self,$dir) = @_;
108 9         11 my $rv;
109 9 100       25 if (-d $self->path($dir)) {
110 5         14 $rv = $self->{directories}{$dir} = 1;
111             } else {
112 4         30 $rv = $self->{directories}{$dir} = 0;
113             }
114 9         53 return $rv;
115             }
116              
117             sub clean {
118 15     15 1 45 my $self = shift;
119 15         23 foreach my $file ( keys %{$self->{files}} ) {
  15         63  
120 19         49 unlink $self->path($file);
121             };
122 15         32 foreach my $dir ( keys %{$self->{directories}} ) {
  15         56  
123 18         39 rmdir $self->path($dir);
124             };
125 15         1970 rmdir $self->{dir};
126             }
127            
128             sub _path_map {
129 13     13   24 my $self = shift;
130 13         18 my %path;
131 13         21 while (my ($k,$v) = each %{$self->{files}}) {
  36         137  
132 23         46 $path{ $self->name($k) } = $v;
133             };
134 13         24 while (my ($k,$v) = each %{$self->{directories}}) {
  26         90  
135 13         30 $path{ $self->name($k) } = $v;
136             };
137 13         33 return \%path;
138             }
139              
140             sub count_unknown {
141 4     4 1 166 my $self = shift;
142 4         16 my $path = $self->_path_map;
143 4 100       177 opendir my($dh), $self->{dir} or croak "$self->{dir}: $!";
144              
145 3         5 my $count = 0;
146 3         94 while (my $file = readdir($dh)) {
147 17 100       59 next if $file eq '.';
148 14 100       31 next if $file eq '..';
149 11 100       55 next if $path->{$file};
150 4         16 ++ $count;
151             }
152 3         54 return $count;
153             };
154              
155             sub count_missing {
156 2     2 1 8 my $self = shift;
157              
158 2         5 my $count = 0;
159 2         4 while (my($file,$has) = each %{$self->{files}}) {
  10         55  
160 8 100 100     24 ++ $count if ($has and not(-f $self->path($file)));
161             }
162 2         5 while (my($file,$has) = each %{$self->{directories}}) {
  5         17  
163 3 100 100     12 ++ $count if ($has and not(-d $self->path($file)));
164             }
165 2         10 return $count;
166             }
167              
168              
169             sub remove_files {
170 2     2 1 6 my $self = shift;
171 2         5 my $count = 0;
172 2         8 foreach my $file (@_) {
173 2         10 my $path = $self->path($file);
174 2         11 $self->{files}{$file} = 0;
175 2         194 $count += unlink($path);
176             }
177 2         43 return $count;
178             }
179              
180             sub remove_directories {
181 3     3 1 9 my $self = shift;
182 3         6 my $count = 0;
183 3         8 foreach my $file (@_) {
184 3         9 my $path = $self->path($file);
185 3         11 $self->{directories}{$file} = 0;
186 3 100       269 $count ++ if rmdir($path);
187             }
188 3         17 return $count;
189             }
190              
191             ##############################
192             # Test Functions
193             ##############################
194              
195             sub has {
196 6     6 1 2993 my ($self,$file,$text) = @_;
197 6 100       35 $text = "Has file $file." unless defined $text;
198 6         32 $self->builder->ok( $self->check_file($file), $text );
199             }
200              
201             sub hasnt {
202 5     5 1 3326 my ($self,$file,$text) = @_;
203 5 100       31 $text = "Doesn't have file $file." unless defined $text;
204 5         46 $self->builder->ok( not($self->check_file($file)), $text );
205             }
206              
207             sub has_dir {
208 4     4 1 1227 my ($self,$file,$text) = @_;
209 4 100       16 $text = "Has directory $file." unless defined $text;
210 4         15 $self->builder->ok( $self->check_directory($file), $text );
211             }
212              
213             sub hasnt_dir {
214 3     3 1 1315 my ($self,$file,$text) = @_;
215 3 100       14 $text = "Doesn't have directory $file." unless defined $text;
216 3         10 $self->builder->ok( not($self->check_directory($file)), $text );
217             }
218              
219             sub clean_ok {
220 2     2 1 1792 my ($self,$text) = @_;
221 2         8 $self->builder->ok($self->clean, $text);
222             }
223              
224             sub _check_dir {
225 9     9   19 my ($dir, $path, $unknown) = @_;
226 9 100       311 opendir my($dh), $dir or croak "$dir: $!";
227              
228 8         207 while (my $file = readdir($dh)) {
229 30 100       174 next if $file eq '.';
230 22 100       67 next if $file eq '..';
231 14 100       105 next if $path->{$file};
232 3         16 push @$unknown, $file;
233             }
234             };
235              
236             sub _check_subdir {
237 4     4   11 my ($self, $dir, $path, $unknown) = @_;
238 4 50       23 opendir my($dh), $self->path($dir) or croak "$self->path(dir): $!";
239              
240 4         85 while (my $file = readdir($dh)) {
241 12 100       106 next if $file eq '.';
242 8 100       27 next if $file eq '..';
243 4         13 my $name = $self->name("$dir/$file");
244 4 100       23 next if $path->{ $name };
245 1         5 push @$unknown, $name;
246             }
247             };
248              
249             sub is_ok {
250 9     9 1 3320 my $self = shift;
251 9         17 my $name = shift;
252 9         102 my $test = $self->builder;
253 9 100       110 $name = "Directory is consistent" unless defined $name;
254              
255 9         37 my @miss;
256 9         17 while (my($file,$has) = each %{$self->{files}}) {
  23         111  
257 14 100 100     58 if ($has and not(-f $self->path($file))) {
258 1         5 push @miss, $file;
259             }
260             }
261 9         17 my @miss_d;
262 9         16 while (my($file,$has) = each %{$self->{directories}}) {
  17         70  
263 8 100 100     36 if ($has and not(-d $self->path($file))) {
264 2         9 push @miss_d, $file;
265             }
266             }
267              
268              
269 9         33 my $path = $self->_path_map;
270 9         11 my @unknown;
271              
272 9         50 _check_dir($self->{dir}, $path, \@unknown);
273 8         14 while (my($file,$has) = each %{$self->{directories}}) {
  16         71  
274 8         24 my $dir = $self->path($file);
275 8 100 100     125 if ($has and -d $dir) {
276 4         18 $self->_check_subdir($file, $path, \@unknown);
277             }
278             }
279              
280 8         47 my $rv = $test->ok((@miss+@unknown+@miss_d) == 0, $name);
281 8 100       9649 unless ($rv) {
282 4         16 $test->diag("Missing file: $_") foreach @miss;
283 4         87 $test->diag("Missing directory: $_") foreach @miss_d;
284 4         152 $test->diag("Unknown file: $_") foreach @unknown;
285             }
286 8         327 return $rv;
287             }
288              
289              
290              
291             1;
292             __END__
293              
294             =head1 NAME
295              
296             Test::Directory - Perl extension for maintaining test directories.
297              
298             =head1 SYNOPSIS
299              
300             use Test::Directory
301             use My::Module
302              
303             my $dir = Test::Directory->new($path);
304             $dir->touch($src_file);
305             My::Module::something( $dir->path($src_file), $dir->path($dst_file) );
306             $dir->has_ok($dst_file); #did my module create dst?
307             $dir->hasnt_ok($src_file); #is source still there?
308              
309             =head1 DESCRIPTION
310              
311             Testing code can involve making sure that files are created and deleted as
312             expected. Doing this manually can be error prone, as it's easy to forget a
313             file, or miss that some unexpected file was added. This module simplifies
314             maintaining test directories by tracking their status as they are modified
315             or tested with this API, making it simple to test both individual files, as
316             well as to verify that there are no missing or unknown files.
317              
318             The idea is to use this API to create a temporary directory and
319             populate an initial set of files. Then, whenever something in the directory
320             is changes, use the test methods to verify that the change happened as
321             expected. At any time, it is simple to verify that the contents of the
322             directory are exactly as expected.
323              
324             Test::Directory implements an object-oriented interface for managing test
325             directories. It tracks which files it knows about (by creating or testing
326             them via its API), and can report if any files were missing or unexpectedly
327             added.
328              
329             There are two flavors of methods for interacting with the directory. I<Utility>
330             methods simply return a value (i.e. the number of files/errors) with no
331             output, while the I<Test> functions use L<Test::Builder> to produce the
332             approriate test results and diagnostics for the test harness.
333              
334              
335             The directory will be automatically cleaned up when the object goes out of
336             scope; see the I<clean> method below for details.
337              
338             =head2 CONSTRUCTOR
339              
340             =over
341              
342             =item B<new>([I<$path>, I<$options>, ...])
343              
344             Create a new instance pointing to the specified I<$path>. I<$options> is
345             an optional hashref of options.
346              
347             I<$path> will be created (or the constructor will die). If I<$path> is
348             undefined, a unique path will be automatically generated; otherwise it is an
349             error for I<$path> to already exist.
350              
351             =back
352              
353              
354             =head2 UTILITY METHODS
355              
356             =over
357              
358             =item B<touch>(I<$file> ...)
359              
360             Create the specified I<$file>s and track their state.
361              
362             =item B<create>(I<$file>,I<%options>)
363              
364             Create the specified I<$file> and track its state. The I<%options> hash
365             supports the following:
366              
367             =over 8
368              
369             =item B<time> => I<$timestamp>
370              
371             Passed to L<perlfunc/utime> to set the files access and modification times.
372              
373             =item B<content> => I<$data>
374              
375             Write I<$data> to the file.
376              
377             =back
378              
379             =item B<mkdir>(I<$directory>)
380              
381             Create the specified I<$directory>; dies if I<mkdir> fails.
382              
383             =item B<name>(I<$file>)
384              
385             Returns the name of the I<$file>, relative to the directory; including any
386             seperator normalization. I<$file> need not exist. This method is used
387             internally by most other methods to translate file paths.
388              
389             For portability, this method implicitly splits the path on UNIX-style /
390             seperators, and rejoins it with the local directory seperator.
391              
392             Absent any seperator substitution, the returned value would be equivalent to
393             I<$file>.
394              
395             =item B<path>(I<$file>)
396              
397             Returns the path for the I<$file>, including the directory name and any
398             substitutions. I<$file> need not exist.
399              
400             =item B<check_file>(I<$file>)
401              
402             Checks whether the specified I<$file> exists, and updates its state
403             accordingly. Returns true if I<$file> exists, false otherwise.
404              
405             This method is used internally by the corresponding test methods.
406              
407             =item B<check_directory>(I<$directory>)
408              
409             Checks whether the specified I<$directory> exists, and updates its state
410             accordingly. Returns true if I<$directory> exists, false otherwise.
411              
412             This method is used internally by the corresponding test methods.
413              
414             Note that replacing a file with a directory, or vice versa, would require
415             calling both I<check_file> and I<check_directory> to update the state to
416             reflect both changes.
417              
418             =item B<remove_files>(I<$file>...)
419              
420             Remove the specified $I<file>s; return the number of files removed.
421              
422             =item B<remove_directories>(I<$directory>...)
423              
424             Remove the specified $I<directories>s; return the number of directories removed.
425              
426             =item B<clean>
427              
428             Remove all known files, then call I<rmdir> on the directory; returns the
429             status of the I<rmdir>. The presence of any unknown files will cause the
430             rmdir to fail, leaving the directory with these unknown files.
431              
432             This method is called automatically when the object goes out of scope.
433              
434             =item B<count_unknown>
435              
436             =item B<count_missing>
437              
438             Returns a count of the unknown or missing files and directories. Note that
439             files and directores are interchangeable when counting missing files, but
440             not when counting unknown files.
441              
442             =back
443              
444             =head2 TEST METHODS
445              
446             The test methods validate the state of the test directory, calling
447             L<Test::Builder>'s I<ok> and I<diag> methods to generate output.
448              
449             =over
450              
451             =item B<has> (I<$file>, I<$test_name>)
452              
453             =item B<hasnt>(I<$file>, I<$test_name>)
454              
455             Verify the status of I<$file>, and update its state. The test will pass if
456             the state is expected. If I<$test_name> is undefined, a default will be
457             generated.
458              
459             =item B<has_dir> (I<$directory>, I<$test_name>);
460              
461             =item B<hasnt_dir>(I<$directory>, I<$test_name>);
462              
463             Verify the status of I<$directory>, and update its state. The test will
464             pass if the state is expected. If I<$test_name> is undefined, a default will be
465             generated.
466              
467             =item B<is_ok>(I<$test_name>)
468              
469             Pass if the test directory has no missing or extra files.
470              
471             =item B<clean_ok>([I<$test_name>])
472              
473             Equivalent to ok(clean,I<$test_name>)
474              
475             =back
476              
477             =head2 EXAMPLES
478              
479             =head3 Calling an external program to move a file
480              
481             $dir->touch('my-file.txt');
482             system ('gzip', $dir->path('my-file.txt'));
483             $dir->has ('my-file.txt.gz', '.gz file is added');
484             $dir->hasnt('my-file.txt', '.txt file is removed');
485             $dir->is_ok; #verify no other changes to $dir
486              
487             =head1 SEE ALSO
488              
489             L<Test::Builder>
490              
491             =head1 AUTHOR
492              
493             Steve Sanbeg, E<lt>sanbeg@cpan.org<gt>
494              
495             =head1 COPYRIGHT AND LICENSE
496              
497             Copyright (C) 2013 by Steve Sanbeg
498              
499             This library is free software; you can redistribute it and/or modify
500             it under the same terms as Perl itself, either Perl version 5.10.1 or,
501             at your option, any later version of Perl 5 you may have available.
502              
503             =cut