File Coverage

blib/lib/Test/Filesystem.pm
Criterion Covered Total %
statement 36 159 22.6
branch 0 36 0.0
condition 0 24 0.0
subroutine 12 25 48.0
pod 1 1 100.0
total 49 245 20.0


line stmt bran cond sub pod time code
1             package Test::Filesystem;
2              
3 1     1   79233 use 5.006;
  1         5  
  1         66  
4 1     1   6 use strict;
  1         3  
  1         39  
5 1     1   6 use warnings;
  1         7  
  1         42  
6              
7 1     1   39639 use Data::Dumper;
  1         12050  
  1         82  
8              
9 1     1   9 use Test::Builder::Module 0.98;
  1         35  
  1         8  
10              
11 1     1   1071 use Test::Deep;
  1         11008  
  1         774  
12              
13             our $VERSION = '0.01';
14              
15             our @ISA = qw(Test::Builder::Module);
16             our @EXPORT = qw(
17             cmp_filesystem
18             );
19              
20             1;
21              
22             =head1 NAME
23              
24             Test::Filesystem - Tester for filesystem content
25              
26             =head1 SYNOPSIS
27              
28             use Test::Testfilesystem tests => 1;
29              
30             cmp_filesystem($got_root_dir, $expected_root_dir, $test_name);
31             # or
32             cmp_filesystem( { OPTIONS }, $got_root_dir, $expected_root_dir, $test_name);
33              
34             =head1 DESCRIPTION
35              
36             This test framework compare filesystems (content and meta attribute).
37             You can use it if your perl program
38             is generating files and you don't want to compare the test results file by file.
39              
40             =head1 PUBLIC INTERFACE
41              
42             =over 4
43              
44             =item C
45              
46             Compares the two given filesystems: C and C. C are unsupported
47             yet.
48              
49             In the current implementation we compare the following attributes:
50              
51             =over 4
52              
53             =item 1. files and directories
54              
55             Location relative to the given root directory. No symlinks or special files will be checked.
56              
57             =item 2. stat attributes
58              
59             C attributes B C, C, C, C and C
60             (inode number).
61              
62             =item 3. file content.
63              
64             Doing the MD5 dance with every file.
65              
66             =back
67              
68             We're not checking the root directory itself, only the content of it. So if the
69             mtime from C and C differs, it will checked.
70              
71             =cut
72              
73             sub cmp_filesystem
74             {
75 0 0   0 1   my $options = ref $_[ 0 ] eq 'HASH' ? shift : {};
76              
77 0           my ( $got, $expected, $name ) = @_;
78              
79 0           my $tb = Test::More->builder;
80              
81 0           my $me = Test::Filesystem::Root->new( root_directory => $got );
82 0           $me->scan();
83 0           my $other = Test::Filesystem::Root->new( root_directory => $expected );
84 0           $other->scan();
85              
86 0           my $files = scalar( @{ $me->files } );
  0            
87              
88 0 0         if ( !$name )
89             {
90 0           $name = sprintf( 'compared %s directory entries', $files );
91             }
92             else
93             {
94 0           $name .= sprintf( ' (compared %s directory entries)', $files );
95             }
96              
97 0           my $diffs = $me->changed_files_structure( $other );
98 0 0         _print_diagnostics( $name, $diffs ) || return 0;
99              
100 0           $diffs = $me->changed_files_content( $other );
101 0 0         _print_diagnostics( $name, $diffs ) || return 0;
102              
103 0           $tb->ok( 1, $name );
104              
105 0           return 1;
106             }
107              
108             sub _print_diagnostics
109             {
110 0     0     my ( $name, $diffs ) = @_;
111              
112 0           my $tb = Test::More->builder;
113              
114 0 0         if ( @$diffs )
115             {
116 0           foreach my $diff ( @$diffs )
117             {
118 0           $tb->diag( _format_diagnostic_lines( $diff ) );
119             }
120 0           $tb->ok( 0, $name );
121 0           return 0;
122             }
123 0           return 1;
124             }
125              
126             sub _format_diagnostic_lines
127             {
128 0     0     my $data = shift;
129              
130             _format_single_diagnostic_line( 'got',
131             $data->{ file_a } . ': ' . $data->{ message_a } )
132             . _format_single_diagnostic_line( 'expected',
133 0           $data->{ file_b } . ': ' . $data->{ message_b } );
134             }
135              
136             sub _format_single_diagnostic_line
137             {
138 0     0     my $key = shift;
139 0           my $message = shift;
140              
141 0           sprintf( "%12s: %s\n", $key, $message );
142             }
143              
144             =back
145              
146             =head1 EXPORT
147              
148             C by default.
149              
150             =head1 TODO
151              
152             =over 4
153              
154             =item Support for C<.tar.gz>.
155              
156             =item Support for filelists (instead a giving a root directory)
157              
158             =back
159              
160             Got ideas? Send them to me.
161              
162             =head1 AUTHOR
163              
164             Erik Wasser, C<< >>
165              
166             =head1 BUGS
167              
168             Please report any bugs or feature requests to C, or through
169             the web interface at L. I will be n
170             otified, and then you'll
171             automatically be notified of progress on your bug as I make changes.
172              
173             =head1 SUPPORT
174              
175             You can find documentation for this module with the perldoc command.
176              
177             perldoc Test::Filesystem
178              
179             You can also look for information at:
180              
181             =over 4
182              
183             =item * RT: CPAN's request tracker (report bugs here)
184              
185             L
186              
187             =item * AnnoCPAN: Annotated CPAN documentation
188            
189             L
190            
191             =item * CPAN Ratings
192              
193             L
194              
195             =item * Search CPAN
196              
197             L
198              
199             =back
200              
201             =head1 SEE ALSO
202              
203             L.
204              
205             =cut
206              
207             package Test::Filesystem::Root;
208              
209 1     1   16 use strict;
  1         2  
  1         30  
210 1     1   5 use warnings;
  1         2  
  1         32  
211              
212 1     1   6 use Data::Dumper;
  1         2  
  1         116  
213              
214 1     1   7 use Digest::MD5;
  1         1  
  1         44  
215 1     1   994 use File::stat qw//;
  1         10026  
  1         32  
216 1     1   6781 use IO::Dir;
  1         19128  
  1         1709  
217              
218             sub new
219             {
220 0     0     my $class = shift;
221 0           my $opt = { @_ };
222             my $self = {
223             root_directory => $opt->{ root_directory },
224 0           files => [],
225             };
226 0           bless( $self, $class );
227 0           return $self;
228             }
229              
230             sub scan
231             {
232 0     0     my $self = shift;
233              
234             $self->{ files } =
235 0           $self->_collect_files( { root_directory => $self->{ root_directory } } );
236             }
237              
238             sub complete_filename
239             {
240 0     0     my $self = shift;
241 0           my $name = shift;
242              
243 0           return $self->{ root_directory } . '/' . $name;
244             }
245              
246             sub _collect_files
247             {
248 0     0     my $opt = shift;
249              
250 0           my $root_path = $opt->{ root_directory };
251              
252 0           my @pathes = ( '.' );
253              
254 0           my @files = ();
255              
256 0           while ( my $path = pop @pathes )
257             {
258 0           my $handle = IO::Dir->new( $root_path . '/' . $path );
259              
260 0           while ( defined( $_ = $handle->read ) )
261             {
262 0 0         next if ( /^\.\.?$/ );
263              
264 0           my $name = $path . '/' . $_;
265 0           my $full_name = $root_path . '/' . $path . '/' . $_;
266              
267 0           push @files, $name;
268              
269 0 0         if ( -d $full_name )
270             {
271 0           push @pathes, $name;
272             }
273             }
274             }
275             # Cut of the beginning './'
276 0           [ sort map { substr( $_, 2 ) } @files ];
  0            
277             }
278              
279             sub changed_files_structure
280             {
281 0     0     my $self = shift;
282 0           my $other = shift;
283              
284 0           my $fs_a = { map { $_, undef } @{ $self->files } };
  0            
  0            
285 0           my $fs_b = { map { $_, undef } @{ $other->files } };
  0            
  0            
286              
287 0           my @changes = ();
288              
289 0           foreach my $file ( sort ( keys %$fs_a, keys %$fs_b ) )
290             {
291 0 0 0       push @changes,
292             {
293             file_a => $self->complete_filename( $file ),
294             message_a => 'exists',
295             file_b => $other->complete_filename( $file ),
296             message_b => 'missing',
297             }
298             if ( exists $fs_a->{ $file } && !exists $fs_b->{ $file } );
299              
300 0 0 0       push @changes,
301             {
302             file_a => $self->complete_filename( $file ),
303             message_a => 'missing',
304             file_b => $other->complete_filename( $file ),
305             message_b => 'exists',
306             }
307             if ( !exists $fs_a->{ $file } && exists $fs_b->{ $file } );
308             }
309              
310 0           return [ sort { $a->{ file_a } cmp $b->{ file_a } } @changes ];
  0            
311             }
312              
313             sub changed_files_content
314             {
315 0     0     my $self = shift;
316 0           my $other = shift;
317              
318 0           my $fs_a = { map { $_, undef } @{ $self->files } };
  0            
  0            
319 0           my $fs_b = { map { $_, undef } @{ $other->files } };
  0            
  0            
320              
321 0           my @removed = ();
322              
323 0           my @diffs = ();
324              
325 0           foreach my $file ( sort keys %$fs_a )
326             {
327 0           my $filename_a = $self->{ root_directory } . '/' . $file;
328 0           my $filename_b = $other->{ root_directory } . '/' . $file;
329              
330 0           my $stat_a = File::stat::stat( $filename_a );
331 0           my $stat_b = File::stat::stat( $filename_b );
332              
333             # Skip if both files are now missing
334 0 0 0       next if ( !$stat_a && !$stat_b );
335              
336 0 0 0       if ( $stat_a && !$stat_b )
337             {
338 0           push @diffs,
339             {
340             file_a => $filename_a,
341             message_a => 'exists',
342             file_b => $filename_b,
343             message_b => 'missing',
344             };
345              
346 0           next;
347             }
348              
349 0 0 0       if ( !$stat_a && $stat_b )
350             {
351 0           push @diffs,
352             {
353             file_a => $filename_a,
354             message_b => 'missing',
355             file_b => $filename_b,
356             message_b => 'exists',
357             };
358              
359 0           next;
360             }
361              
362 0           my $message = $self->_changed_stats(
363             {
364             file_a => $filename_a,
365             stat_a => $stat_a,
366             file_b => $filename_b,
367             stat_b => $stat_b,
368             }
369             );
370              
371 0 0         if ( $message )
372             {
373 0           push @diffs, $message;
374 0           next;
375             }
376              
377 0           my $handle_a = IO::File->new( $filename_a, '<' );
378 0           my $handle_b = IO::File->new( $filename_b, '<' );
379              
380 0           my $hexdigest_a = Digest::MD5->new->addfile( $handle_a )->hexdigest;
381 0           my $hexdigest_b = Digest::MD5->new->addfile( $handle_b )->hexdigest;
382              
383 0 0         if ( $hexdigest_a ne $hexdigest_b )
384             {
385 0           push @diffs,
386             {
387             file_a => $filename_a,
388             message_a => sprintf( "MD5 is %s'", $hexdigest_a ),
389             file_b => $filename_b,
390             message_b => sprintf( "MD5 is %s'", $hexdigest_b ),
391             };
392 0           next;
393             }
394              
395             }
396 0           \@diffs;
397             }
398              
399             sub _changed_stats
400             {
401 0     0     my $self = shift;
402 0           my $opt = shift;
403              
404 0           foreach my $stat_options ( @{ _stat_options() } )
  0            
405             {
406             next
407             if ( exists $stat_options->{ flags }
408 0 0 0       && $stat_options->{ flags }->{ ignore } );
409              
410 0           my $method = $stat_options->{ method };
411              
412 0           my $stat_option_a = $opt->{ stat_a }->$method();
413 0           my $stat_option_b = $opt->{ stat_b }->$method();
414              
415 0 0 0       if ( defined $stat_option_a && !defined $stat_option_b )
416             {
417             return {
418             file_a => $opt->{ file_a },
419             message_a => sprintf( "Attribute '%s' is %s'", $stat_option_a ),
420             file_b => $opt->{ file_b },
421 0           message_b => sprintf( "Attribute '%s' is %s'", 'undef' )
422             };
423             }
424              
425 0 0 0       if ( !defined $stat_option_a && defined $stat_option_b )
426             {
427             return {
428             file_a => $opt->{ file_a },
429             message_a => sprintf( "Attribute '%s' is %s'", 'undef' ),
430             file_b => $opt->{ file_b },
431 0           message_b => sprintf( "Attribute '%s' is %s'", $stat_option_b )
432             };
433             }
434              
435 0 0         if ( $stat_option_a ne $stat_option_b )
436             {
437             return {
438             file_a => $opt->{ file_a },
439             message_a =>
440             sprintf( "attribute '%s' is %s'", $method, $stat_option_a ),
441             file_b => $opt->{ file_b },
442 0           message_b =>
443             sprintf( "attribute '%s' is %s'", $method, $stat_option_b )
444             };
445             }
446             }
447 0           return;
448             }
449              
450             sub _stat_options
451             {
452 0     0     my $c = [
453             {
454             method => 'dev',
455             message => 'device number of filesystem',
456             flags => { ignore => 1 },
457             },
458             {
459             method => 'ino',
460             message => 'inode number',
461             flags => { ignore => 1 },
462             },
463             {
464             method => 'mode',
465             message => 'file mode (type and permissions)',
466             },
467             {
468             method => 'nlink',
469             message => 'number of (hard) links to the file',
470             },
471             {
472             method => 'uid',
473             message => 'numeric user ID of file\'s owner',
474             },
475             {
476             method => 'gid',
477             message => 'numeric group ID of file\'s owner',
478             },
479             {
480             method => 'rdev',
481             message => 'the device identifier (special files only)',
482             },
483             {
484             method => 'size',
485             message => 'total size of file, in bytes',
486             },
487             {
488             method => 'atime',
489             message => 'last access time in seconds since the epoch',
490             },
491             {
492             method => 'mtime',
493             message => 'last modify time in seconds since the epoch',
494             },
495             {
496             method => 'ctime',
497             message => 'inode change time in seconds since the epoch',
498             flags => { ignore => 1 },
499              
500             },
501             {
502             method => 'blksize',
503             message => 'preferred block size for file system I/O',
504             flags => { ignore => 1 },
505             },
506             {
507             method => 'blocks',
508             message => 'actual number of blocks allocated',
509             flags => { ignore => 1 },
510              
511             },
512             ];
513 0           return $c;
514             }
515              
516             sub files
517             {
518 0     0     my $self = shift;
519              
520 0           $self->{ files };
521             }
522