File Coverage

blib/lib/Test/Virtual/Filesystem.pm
Criterion Covered Total %
statement 740 827 89.4
branch 102 210 48.5
condition 8 37 21.6
subroutine 85 89 95.5
pod 7 7 100.0
total 942 1170 80.5


line stmt bran cond sub pod time code
1             #######################################################################
2             # $URL: svn+ssh://equilibrious@equilibrious.net/home/equilibrious/svnrepos/chrisdolan/Test-Virtual-Filesystem/lib/Test/Virtual/Filesystem.pm $
3             # $Date: 2008-07-27 21:28:05 -0500 (Sun, 27 Jul 2008) $
4             # $Author: equilibrious $
5             # $Revision: 785 $
6             ########################################################################
7              
8             package Test::Virtual::Filesystem;
9              
10 3     3   100052 use warnings;
  3         6  
  3         96  
11 3     3   17 use strict;
  3         5  
  3         100  
12 3     3   80 use 5.008;
  3         16  
  3         128  
13              
14 3     3   2519 use English qw(-no_match_vars);
  3         10982  
  3         16  
15 3     3   1361 use Carp qw(croak);
  3         6  
  3         175  
16 3     3   18 use File::Spec;
  3         6  
  3         78  
17 3     3   3519 use List::MoreUtils qw(any);
  3         6484  
  3         262  
18 3     3   4816 use Attribute::Handlers;
  3         16720  
  3         20  
19 3     3   101 use Config;
  3         6  
  3         138  
20 3     3   2796 use POSIX qw(:errno_h strerror);
  3         25659  
  3         42  
21 3     3   15291 use Readonly;
  3         15819  
  3         248  
22 3     3   27 use Test::More;
  3         6  
  3         39  
23 3     3   1055 use base 'Test::Class';
  3         6  
  3         5252  
24              
25             our $VERSION = '0.13';
26              
27             Readonly::Scalar my $TIME_LENIENCE => 2; # seconds of tolerance between CPU clock and disk mtime
28              
29             # Currently this must not nest more than one level deep!
30             # (due to implementation of deep copy in new() and the static accessor/mutator constructor)
31             Readonly::Hash my %feature_defaults => (
32             xattr => 0,
33             time => {
34             atime => 0,
35             mtime => 1,
36             ctime => 1,
37             },
38             permissions => 0,
39             special => {
40             fifo => 0,
41             },
42             symlink => 1,
43             hardlink => {
44             nlink => 1,
45             },
46             chown => 0,
47             );
48              
49             # if true, the feature is disabled no matter what. For example, most versions
50             # of Windows at this writing do not support symlinks at all, regardless of
51             # whether your virtual filesystem supports them
52             Readonly::Hash my %feature_disabled => (
53             $Config{d_symlink} ? () : (symlink => 1),
54             $Config{d_chown} ? () : (chown => 1),
55             eval {require File::ExtAttr; 1;} ? () : (xattr => 1),
56             );
57              
58             =pod
59              
60             =for stopwords TODO CPAN MSWin32
61              
62             =head1 NAME
63              
64             Test::Virtual::Filesystem - Validate a filesystem
65              
66             =head1 SYNOPSIS
67              
68             use Test::Virtual::Filesystem;
69             Test::Virtual::Filesystem->new({mountdir => '/path/to/test'})->runtests;
70              
71             or with more customization:
72              
73             use Test::Virtual::Filesystem;
74             my $test = Test::Virtual::Filesystem->new({mountdir => '/path/to/test', compatible => '0.03'});
75             $test->enable_test_xattr(1);
76             $test->enable_test_chown(1);
77             $test->enable_test_atime(1);
78             $test->runtests;
79              
80             See the file F in this distribution or the file F in
81             the L distribution for thorough examples.
82              
83             WARNING: all of the files in the C will be deleted in the C
84             method so BE CAREFUL that you specify the right folder!
85              
86             =head1 LICENSE
87              
88             Copyright 2008 Chris Dolan, I
89              
90             This library is free software; you can redistribute it and/or modify it
91             under the same terms as Perl itself.
92              
93             =head1 DESCRIPTION
94              
95             If you are creating a filesystem, say via L or L, you
96             need a fairly mundane set of tests to try out lots of typical filesystem
97             operations. This package attempts to accumulate a bunch of those tests into a
98             handy suite to make it easier for you to test your filesystem.
99              
100             This suite is based on C, a fantastic library for organizing
101             tests into bite-sized bundles. The power of Test::Class lets you select a
102             subset of tests to run at author time. For example, when I was working on the
103             extended attribute (aka C) tests, I found myself typing this:
104              
105             env TEST_METHOD='xattr_.*' perl -Ilib t/filesys.t
106              
107             which runs just the test methods that begin with C.
108              
109             There are several methods that let you turn on or off a subset of the tests.
110             For example, if you do not intend that your filesystem will support symbolic
111             links, you can invoke C<$test->enable_test_symlink(0)> in your test program
112             just before you call C<$test->runtests>.
113              
114             =head1 COMPATIBILITY POLICY
115              
116             Every time I add a new test to this suite, I annotate it with a
117             version number. If client code specifies an expected version number
118             (say, 1.10) and it's running against a newer version or this module
119             (say, 1.20) then any newer test will be marked as a TODO test. That
120             way if the test fails, it won't regress published code that used to
121             work.
122              
123             This policy will allow us to continue adding new filesystem tests
124             without worrying about breaking existing CPAN modules.
125              
126             =head1 CAVEATS AND LIMITATIONS
127              
128             This module needs a more complete suite of test cases. In particular, tests
129             are needed for the following filesystem features:
130              
131             hardlinks
132             nlink
133             seek/rewinddir, tell/telldir
134             read, sysread, syswrite
135             overwrite (with open '+<')
136             deep directories
137             very full directories
138             large files
139             filenames with spaces
140             non-ASCII filenames (maybe constructor should specify the encoding?)
141             permissions
142             special file types (fifos, sockets, character and block devices, etc)
143             chown
144             binmode, non-binmode
145             eof
146             fileno
147             statfs (AKA `df` or `mount`)
148             rename corner cases:
149             * dest inside src
150             * src or dest leaf is '.' or '..'
151             * src or dest is FS root
152             * dest leaf is symlink
153             threading and re-entrancy
154             file locking?
155             async I/O??
156              
157             Any help writing tests (or adapting tests from existing suites) will
158             be appreciated!
159              
160             =head1 METHODS
161              
162             This module is a subclass of L. All methods from that class are
163             available, particularly C.
164              
165             =over
166              
167             =item $pkg->new({mountdir =E $mountdir, ...})
168              
169             Create a new test suite which will operate on files contained within the
170             specified mount directory. WARNING: any and all files and folders in that
171             mount directory will be deleted!
172              
173             The supported options are:
174              
175             =over
176              
177             =item C
178              
179             This required property indicates where tests should run.
180              
181             =item C
182              
183             Specify a Test::Virtual::Filesystem version number that is known to
184             work. If the actual Test::Virtual::Filesystem version number is
185             greater, then any test cases added after the specified compatible
186             version are considered C tests. See L for details
187             about C tests.
188              
189             =back
190              
191             =item $self->init()
192              
193             Invoked just before then end of C. This exists solely for
194             subclassing convenience. This implementation does nothing.
195              
196             =back
197              
198             =head1 PROPERTIES
199              
200             The following accessor/mutator methods exist to turn on/off various
201             features. They all behave in usual Perl fashion: with no argument,
202             they return the current value. With one argument, they set the
203             current value and return the newly set value.
204              
205             =over
206              
207             =item $self->enable_test_all()
208              
209             As a getter, checks whether all of the other tests are enabled.
210              
211             As a setter, turns on/off all the tests.
212              
213             =item $self->enable_test_xattr()
214              
215             Default false.
216              
217             =item $self->enable_test_time()
218              
219             Default true. If set false, it also sets C, C and C false.
220              
221             =item $self->enable_test_atime()
222              
223             Default false.
224              
225             =item $self->enable_test_mtime()
226              
227             Default true.
228              
229             =item $self->enable_test_ctime()
230              
231             Default true.
232              
233             =item $self->enable_test_permissions()
234              
235             Default false.
236              
237             =item $self->enable_test_special()
238              
239             Default true. If set false, it also sets C false.
240              
241             =item $self->enable_test_fifo()
242              
243             Default false. AKA named pipes.
244              
245             =item $self->enable_test_symlink()
246              
247             Default true, except for platforms that do not support symlinks (for example
248             MSWin32 and cygwin) as determined by C<$Config::Config{d_symlink}>.
249              
250             =item $self->enable_test_hardlink()
251              
252             AKA the C function. Default true. If set false, this also sets C false.
253              
254             =item $self->enable_test_nlink()
255              
256             Count hard links. Default true.
257              
258             =item $self->enable_test_chown()
259              
260             Default false.
261              
262             =back
263              
264             =head1 TEST CASES
265              
266             =over
267              
268             =cut
269              
270             sub new {
271 3     3 1 4375 my ($pkg, $opts) = @_;
272 3         38 my $self = $pkg->SUPER::new();
273 3   50     1646 $opts ||= {};
274 3         12 for my $key (qw(mountdir compatible)) {
275 6         23 $self->{$key} = $opts->{$key};
276             }
277 9         72 $self->{fs_opts} = {
278             # one-level deep copy
279 3 100       30 map {$_ => ref $feature_defaults{$_} ? { %{$feature_defaults{$_}} } : $feature_defaults{$_}}
  21         482  
280             keys %feature_defaults,
281             };
282 3         120 $self->init;
283 3         7 $self->{ntestdir} = 0;
284 3         26 return $self;
285             }
286              
287             sub init {
288             # no-op, subclasses may override
289 3     3 1 8 return;
290             }
291              
292             {
293             # Create a read-write accessor for each enabling feature
294 3     3   53184 no strict 'refs'; ## no critic(NoStrict)
  3         9  
  3         1889  
295             for my $field (keys %feature_defaults) {
296             *{'enable_test_'.$field} = sub {
297 0 0   0   0 return $_[0]->{fs_opts}->{$field} if @_ == 1;
298 0 0       0 return $_[0]->{fs_opts}->{$field} = $_[1] if @_ == 2;
299 0         0 croak 'wrong number of arguments to ' . $field;
300             };
301             my $val = $feature_defaults{$field};
302             if (ref $val) {
303             for my $subfield (keys %{$val}) {
304             *{'enable_test_'.$subfield} = sub {
305 0 0 0 0   0 return $_[0]->{fs_opts}->{$field} && $_[0]->{fs_opts}->{$field}->{$subfield} if @_ == 1;
306 0 0 0     0 return ($_[0]->{fs_opts}->{$field} ||= {})->{$subfield} = $_[1] if @_ == 2;
307 0         0 croak 'wrong number of arguments to ' . $subfield;
308             };
309             }
310             }
311             }
312             }
313              
314             sub enable_test_all {
315 0     0 1 0 my ($self, @arg) = @_;
316 0         0 return $self->_enable_test_all($self->{fs_opts}, @arg);
317             }
318             sub _enable_test_all {
319 0     0   0 my ($self, $hash, @arg) = @_;
320              
321 0         0 my $all_set = 1;
322 0         0 for my $key (keys %{$hash}) {
  0         0  
323 0 0       0 if (ref $hash->{$key}) {
324 0   0     0 $all_set = $self->_enable_test_all($hash->{$key}, @arg) && $all_set; #recurse
325             } else {
326 0 0       0 if (@arg) {
327 0 0       0 $hash->{$key} = $arg[0] ? 1 : 0;
328             }
329 0   0     0 $all_set &&= $hash->{$key};
330             }
331             }
332 0         0 return $all_set;
333             }
334              
335             =item setup()
336              
337             Runs before every test to prepare a directory for testing.
338              
339             =cut
340              
341             sub setup : Test(setup) {
342 48     48 1 18688 my ($self) = @_;
343 48 50       223 if (!defined $self->{mountdir}) {
344 0         0 croak 'Programmer error: you did not specify a mountdir';
345             }
346 48 50       1159 if (!-d $self->{mountdir}) {
347 0         0 croak "Your mountdir '$self->{mountdir}' is not a valid directory";
348             }
349 48 50       719 if (!File::Spec->file_name_is_absolute($self->{mountdir})) {
350 0         0 croak "Your mountdir '$self->{mountdir}' is not an absolute path";
351             }
352 48 50       484 if (File::Spec->splitdir($self->{mountdir}) <= 2) {
353 0         0 croak "Your mountdir '$self->{mountdir}' is too close to the root of the filesystem." .
354             ' I am too scared of deleting important files to use it';
355             }
356 48         651 $self->{tempdir} = File::Spec->catdir($self->{mountdir}, 'testdir' . ++$self->{ntestdir});
357 48         4606 mkdir $self->{tempdir};
358 48 50       1021 if (! -d $self->{tempdir}) {
359 0         0 die 'Failed to create tempdir';
360             }
361 48         246 return;
362 3     3   19 }
  3         5  
  3         19  
363              
364             =item teardown()
365              
366             Runs after every test to clean up the test directory so the next test
367             will have a clean workspace.
368              
369             =cut
370              
371             sub teardown : Test(teardown) {
372 48     48 1 17167 my ($self) = @_;
373 48         189 my $tmpdir = delete $self->{tempdir};
374 48 50 33     1550 if (defined $tmpdir && -e $tmpdir) {
375 48         193 $self->_cleandir($tmpdir);
376 48 50       351 if ($tmpdir ne $self->{mountdir}) {
377 48 50       5368 rmdir $tmpdir or die $OS_ERROR;
378             }
379             }
380 48 50 33     1467 if (defined $tmpdir && -d $tmpdir) {
381 0         0 die 'Failed to remove tempdir';
382             }
383 48         279 return;
384 3     3   1117 }
  3         5  
  3         12  
385              
386             sub _cleandir {
387 66     66   117 my ($self, $dir) = @_;
388 66         520 for my $file ($self->_read_dir($dir)) {
389 182 100       502 next if q{.} eq $file;
390 116 100       270 next if q{..} eq $file;
391 50         6839 my $path = File::Spec->catfile($dir, $file);
392 50 50       268 die 'Internal error: escaped the temp space!' if length $path <= length $self->{mountdir};
393 50 50 66     2284 die 'nonsense missing file: ' . $path if !-l $path && !-e $path;
394 50 100       1966 if (-l $path) {
    100          
395 8 50       689 unlink $path or die $OS_ERROR;
396             } elsif (-d $path) {
397 18         66 $self->_cleandir($path);
398 18 50       1956 rmdir $path or die $OS_ERROR;
399             } else {
400 24 50       3922 unlink $path or die $OS_ERROR;
401             }
402             }
403 66         175 return;
404             }
405              
406             =item Introduced($version)
407              
408             A subroutine attribute used to flag the Test::Virtual::Filesystem
409             version number when that test was introduced. It's used like this:
410              
411             sub open_nonexistent_file : Tests(1) : Introduced('0.02') {
412             ok(!open(my $f, '<', '/tmp/no_such_file'));
413             }
414              
415             =cut
416              
417             # http://use.perl.org/~ChrisDolan/journal/34906
418             # http://use.perl.org/~ChrisDolan/journal/34920
419              
420             sub Introduced : ATTR(CODE) { ## no critic(MixedCase)
421 134     134 1 227721 my ($class, $symbol, $code_ref, $attr, $introduced_version) = @_;
422 134 50       407 if ($symbol eq 'ANON') {
423 0         0 warn 'cannot test anonymous subs - you probably loaded ' . __PACKAGE__ . ' too late.' .
424             ' (after the CHECK block was run)';
425             } else {
426             # Wrap the sub in a version test
427 3     3   1409 no warnings 'redefine'; ## no critic(TestingAndDebugging::ProhibitNoWarnings)
  3         7  
  3         153  
428 134         496 *{$symbol} = sub {
429 3     3   15 no strict 'refs'; ## no critic(TestingAndDebugging::ProhibitNoStrict)
  3         5  
  3         271  
430 43     43   10340 local ${$class.'::TODO'} = $_[0]->_compatible($introduced_version); ## no critic(Local)
  43         406  
431 43         166 $code_ref->(@_);
432 134         468 };
433              
434             #my $name = *{$symbol}{NAME};
435             #print STDERR "record $class\::$name as $introduced_version\n";
436             }
437 134         412 return;
438 3     3   15 }
  3         5  
  3         13  
439             sub _compatible {
440 43     43   81 my ($self, $introduced_version) = @_;
441 43 100       186 return if !$self->{compatible};
442 2 50       9 return if $introduced_version le $self->{compatible};
443 2         7 return 'compatibility mode ' . $self->{compatible};
444             }
445              
446             =item Features($featurelist)
447              
448             This is a subroutine attribute to specify one or more features used in
449             the test. The features should be listed as a comma-separated list:
450              
451             sub symlink_create : Tests(1) : Features('symlink') {
452             ok(symlink($src, $dest));
453             }
454             sub symlink_permissions : Tests(2) : Features('symlink, permissions') {
455             ok(symlink($src, $dest));
456             ok(-w $dest);
457             }
458              
459             Subfeatures must be separated from their parent features by a C. For example:
460              
461             sub atime_mtime_set : Tests(1) : Features('time/atime, time/mtime') {
462             my $now = time;
463             ok(utime($now, $now, $file));
464             }
465              
466             Look at the source code for C<%feature_defaults> to see the supported features and
467             subfeatures. The C methods above describe the all the
468             features, but in those methods the subfeature names are flattened.
469              
470             =cut
471              
472             sub Features : ATTR(CODE) { ## no critic(MixedCase)
473 35     35 1 6909 my ($class, $symbol, $code_ref, $attr, $features) = @_;
474 35 50       112 if ($symbol eq 'ANON') {
475 0         0 warn 'cannot test anonymous subs - you probably loaded ' . $class . ' too late.' .
476             ' (after the CHECK block was run)';
477             } else {
478 35 50       80 my @features = ref $features ? @{$features} : split m/\s*,\s*/xms, $features;
  35         174  
479             # Wrap the sub in a feature test
480 3     3   1990 no warnings 'redefine'; ## no critic(TestingAndDebugging::ProhibitNoWarnings)
  3         7  
  3         343  
481 35         133 *{$symbol} = sub {
482 13     13   2203 my $blocking_feature = _blocking_feature(__PACKAGE__, $_[0], @features);
483 13 100       128 return $blocking_feature if $blocking_feature;
484 8         37 return $code_ref->(@_);
485 35         152 };
486             }
487 35         108 return;
488 3     3   15 }
  3         6  
  3         11  
489             sub _blocking_feature {
490 13     13   44 my ($pkg, $self, @features) = @_;
491              
492 13         31 for my $feature (@features) {
493 13 100       125 return $feature . ' (no OS support)' if $feature_disabled{$feature};
494 11         108 my $opts = $self->{fs_opts};
495 11         46 for my $part (split m{/}xms, $feature) {
496 15 50       45 return $feature if !ref $opts;
497 15 100       51 return $feature if !$opts->{$part};
498 12         42 $opts = $opts->{$part};
499             }
500             }
501 8         22 return;
502             }
503              
504             =item stat_dir(), introduced in v0.01
505              
506             =cut
507              
508             sub stat_dir : Test(6) : Introduced('0.01') {
509 1         4 my ($self) = @_;
510 1         6 my $f = $self->_file(q{/});
511 1         25 ok(-e $f, 'mount dir exists');
512 1         416 ok(-d $f, 'mount dir is a dir');
513 1         301 ok(!-f $f, 'mount dir is not a file');
514 1         303 ok(!-l $f, 'mount dir is not a symlink');
515 1         423 ok(-r $f, 'mount dir is readable');
516 1         340 ok(-x $f, 'mount dir is searchable');
517 1         307 return;
518 3     3   2308 }
  3         5  
  3         15  
519              
520             ## This turned out to be very platform-sensitive.
521             #
522             # =item stat_dir_size(), introduced in v0.02
523             #
524             # =cut
525             #
526             # sub stat_dir_size : Test(1) : Introduced('0.02') {
527             # my ($self) = @_;
528             # my $f = $self->_file(q{/});
529             # ok(-s $f, 'mount dir has non-zero size');
530             # return;
531             # }
532              
533             =item read_dir(), introduced in v0.01
534              
535             =cut
536              
537             sub read_dir : Test(3) : Introduced('0.01') {
538 1         4 my ($self) = @_;
539 1         6 my $f = $self->_file(q{/});
540 1         6 my @files = $self->_read_dir($f);
541 1         9 cmp_ok(scalar @files, '>=', 2, 'dir contains at least two entries');
542 1         614 ok((any { $_ eq q{.} } @files), 'dir contains "."');
  2         9  
543 1         425 ok((any { $_ eq q{..} } @files), 'dir contains ".."');
  1         5  
544 1         408 return;
545 3     3   1113 }
  3         5  
  3         13  
546              
547             =item read_dir_fail(), introduced in v0.01
548              
549             =cut
550              
551             sub read_dir_fail : Test(2) : Introduced('0.01') {
552 1         4 my ($self) = @_;
553 1         5 my $f = $self->_file('/no_such');
554 1         4 eval {
555 1         4 $self->_read_dir_die($f);
556             };
557 1         8 $self->_is_errno($EVAL_ERROR, $OS_ERROR, [ENOENT()], 'read non-existent dir');
558 1         528 ok(!-e $f, 'did not make dir');
559 1         513 return;
560 3     3   975 }
  3         7  
  3         12  
561              
562             =item read_file_fail(), introduced in v0.01
563              
564             =cut
565              
566             sub read_file_fail : Test(2) : Introduced('0.01') {
567 1         3 my ($self) = @_;
568 1         16 my $f = $self->_file('/read_file_fail');
569 1         4 my $content = 'content';
570 1         3 eval {
571 1         4 $self->_read_file_die($f);
572             };
573 1         7 $self->_is_errno($EVAL_ERROR, $OS_ERROR, [ENOENT()], 'read non-existent file');
574 1         547 ok(!-e $f, 'did not make file');
575 1         418 return;
576 3     3   1026 }
  3         5  
  3         16  
577              
578             =item write_empty_file(), introduced in v0.01
579              
580             =cut
581              
582             sub write_empty_file : Test(2) : Introduced('0.01') {
583 1         3 my ($self) = @_;
584 1         16 my $f = $self->_file('/create_file');
585 1         6 $self->_write_file($f);
586 1         25 ok(-f $f, 'created empty file');
587 1         377 is(-s $f, 0, 'file got right size');
588 1         298 return;
589 3     3   923 }
  3         5  
  3         13  
590              
591             =item write_file(), introduced in v0.01
592              
593             =cut
594              
595             sub write_file : Test(2) : Introduced('0.01') {
596 1         2 my ($self) = @_;
597 1         6 my $f = $self->_file('/write_file');
598 1         4 my $content = 'content';
599 1         4 $self->_write_file($f, $content);
600 1         25 ok(-f $f, 'wrote file');
601 1         421 is(-s $f, length $content, 'file got right size');
602 1         337 return;
603 3     3   975 }
  3         17  
  3         14  
604              
605             =item write_file_subdir_fail(), introduced in v0.01
606              
607             =cut
608              
609             sub write_file_subdir_fail : Test(2) : Introduced('0.01') {
610 1         3 my ($self) = @_;
611 1         5 my $f = $self->_file('/no_such/write_file');
612 1         4 my $content = 'content';
613 1         2 eval {
614 1         4 $self->_write_file($f, $content);
615             };
616 1         7 $self->_is_errno($EVAL_ERROR, $OS_ERROR, [ENOENT()], 'write to non-existent folder');
617 1         357 ok(!-f $f, 'did not make file');
618 1         337 return;
619 3     3   1061 }
  3         7  
  3         13  
620              
621             =item write_append_file(), introduced in v0.01
622              
623             =cut
624              
625             sub write_append_file : Test(2) : Introduced('0.01') {
626 1         4 my ($self) = @_;
627 1         5 my $f = $self->_file('/append_file');
628 1         3 my $content = 'content';
629 1         5 $self->_write_file($f, $content);
630 1         8 $self->_append_file($f, $content);
631 1         28 ok(-f $f, 'wrote file');
632 1         439 ok(-s $f == 2 * length $content, 'file got right size');
633 1         325 return;
634 3     3   964 }
  3         5  
  3         18  
635              
636             ## Perl's '>>' creates the file if it doesn't exist
637             #
638             #=item write_append_file_fail(), introduced in v0.01
639             #
640             #=cut
641             #
642             # sub write_append_file_fail : Test(2) : Introduced('0.01') {
643             # my ($self) = @_;
644             # my $f = $self->_file('/append_file_fail');
645             # my $content = 'content';
646             # eval {
647             # $self->_append_file($f, $content);
648             # };
649             # $self->_is_errno($EVAL_ERROR, $OS_ERROR, [ENOENT()], 'append to non-existent file');
650             # ok(!-f $f, 'did not make file');
651             # return;
652             # }
653              
654             =item write_read_file(), introduced in v0.01
655              
656             =cut
657              
658             sub write_read_file : Test(1) : Introduced('0.01') {
659 1         3 my ($self) = @_;
660 1         6 my $f = $self->_file('/read_file');
661 1         4 my $content = 'content';
662 1         8 $self->_write_file($f, $content);
663 1         7 is($self->_read_file($f), $content, 'read file');
664 1         435 return;
665 3     3   1038 }
  3         7  
  3         13  
666              
667             =item write_read_file_binary(), introduced in v0.08
668              
669             =cut
670              
671             sub write_read_file_binary : Test(1) : Introduced('0.08') {
672 1         3 my ($self) = @_;
673 1         5 my $f = $self->_file('/read_file');
674 1         5 my $content = 'content';
675 1         7 for my $ord (0 .. 0xff, 0 .. 0xff) { ## no critic(MagicNumber)
676 512         762 $content .= chr $ord;
677             }
678 1         6 $self->_write_file($f, $content);
679 1         6 is($self->_read_file($f), $content, 'read file');
680 1         385 return;
681 3     3   1149 }
  3         48  
  3         14  
682              
683             =item write_unlink_file(), introduced in v0.01
684              
685             =cut
686              
687             sub write_unlink_file : Test(3) : Introduced('0.01') {
688 1         3 my ($self) = @_;
689 1         5 my $f = $self->_file('/read_file');
690 1         4 my $content = 'content';
691 1         5 $self->_write_file($f, $content);
692 1         23 ok(-e $f, 'file exists');
693 1         1077 ok(-f $f, 'file is a file');
694 1 50       529 unlink $f or die $OS_ERROR;
695 1         25 ok(!-f $f, 'file is deleted');
696 1         285 return;
697 3     3   905 }
  3         4  
  3         10  
698              
699             =item write_mkdir(), introduced in v0.01
700              
701             =cut
702              
703             sub write_mkdir : Test(1) : Introduced('0.01') {
704 1         3 my ($self) = @_;
705 1         4 my $f = $self->_file('/mk_dir');
706 1 50       85 mkdir $f or die $OS_ERROR;
707 1         22 ok(-d $f, 'made dir');
708 1         326 return;
709 3     3   776 }
  3         5  
  3         11  
710              
711             =item write_mkdir_fail(), introduced in v0.01
712              
713             =cut
714              
715             sub write_mkdir_fail : Test(2) : Introduced('0.01') {
716 1         2 my ($self) = @_;
717 1         4 my $f = $self->_file('/no_such/mk_dir');
718 1         3 eval {
719 1 50       43 mkdir $f or die $OS_ERROR;
720             };
721 1         7 $self->_is_errno($EVAL_ERROR, $OS_ERROR, [ENOENT()], 'mkdir at non-existent path');
722 1         330 ok(!-d $f, 'did not make dir');
723 1         393 return;
724 3     3   879 }
  3         4  
  3         13  
725              
726             =item write_rmdir(), introduced in v0.01
727              
728             =cut
729              
730             sub write_rmdir : Test(2) : Introduced('0.01') {
731 1         3 my ($self) = @_;
732 1         5 my $f = $self->_file('/rm_dir');
733 1 50       81 mkdir $f or die $OS_ERROR;
734 1         26 ok(-d $f, 'made dir');
735 1 50       434 rmdir $f or die $OS_ERROR;
736 1         26 ok(!-d $f, 'made dir');
737 1         374 return;
738 3     3   908 }
  3         22  
  3         12  
739              
740             =item write_subdir(), introduced in v0.01
741              
742             =cut
743              
744             sub write_subdir : Test(3) : Introduced('0.01') {
745 1         3 my ($self) = @_;
746 1         7 my $d = $self->_file('/mk_dir');
747 1         5 my $f = $self->_file('/mk_dir/file');
748 1         4 my $content = 'content';
749 1 50       87 mkdir $d or die $OS_ERROR;
750 1         69 ok(-d $d, 'made dir');
751 1         372 $self->_write_file($f, $content);
752 1         35 ok(-f $f, 'wrote file in subdir');
753 1         412 is($self->_read_file($f), $content, 'right content');
754 1         428 return;
755 3     3   1010 }
  3         3  
  3         11  
756              
757             =item symlink_create(), introduced in v0.02
758              
759             =cut
760              
761             sub symlink_create : Test(10) : Introduced('0.02') : Features('symlink') {
762 1         4 my ($self) = @_;
763 1         4 my $target = 'symlink_target';
764 1         7 my $src = $self->_file("/$target");
765 1 50       85 mkdir $src or die $OS_ERROR;
766 1         25 ok(-e $src, 'symlink source exists');
767 1         318 my $s = $self->_file('/symlink_create');
768 1 50       56 ok((symlink $target, $s), 'created symlink') or die $OS_ERROR;
769 1         380 ok(-e $s, 'symlink exists');
770 1         285 ok(-l $s, 'symlink is a symlink');
771 1         279 ok(-d $s, 'symlink src is a dir');
772 1         280 ok(!-f $s, 'symlink src is not a file');
773 1         436 is(-s $s, -s $src, 'symlink size is size of source');
774 1         290 is(readlink $s, $target, 'read newly created symlink');
775 1 50       501 unlink $s or die $OS_ERROR;
776 1         28 ok(!-e $s, 'symlink deleted');
777 1         301 ok(-e $src, 'symlink source not deleted');
778 1         268 return;
779 3     3   1238 }
  3         9  
  3         12  
780              
781             =item symlink_follow(), introduced in v0.04
782              
783             =cut
784              
785             sub symlink_follow : Test(11) : Introduced('0.04') : Features('symlink') {
786 1         2 my ($self) = @_;
787 1         4 my $target = 'symlink_target';
788 1         9 my $srcdir = $self->_file("/$target");
789 1         7 my $srcfile = $self->_file("/$target/file.txt");
790 1         6 my $s = $self->_file('/symlink_follow');
791 1         5 my $symfile = $s . '/file.txt';
792 1 50       82 mkdir $srcdir or die $OS_ERROR;
793 1         3 my $content = 'content';
794 1         6 $self->_write_file($srcfile, $content);
795 1         26 ok(-e $srcfile, 'symlink source exists');
796 1 50       4163 ok((symlink $target, $s), 'created symlink') or die $OS_ERROR;
797 1         812 ok(-e $s, 'symlink exists');
798 1         1201 ok(-l $s, 'symlink is a symlink');
799 1         1231 ok(-d $s, 'symlink src is a dir');
800 1         675 ok(!-f $s, 'symlink src is not a file');
801 1         324 is(-s $symfile, length $content, 'size of file though symlink size is size of content');
802 1         937 is($self->_read_file($symfile), $content, 'read file through newly created symlink');
803 1 50       2173 unlink $symfile or die $OS_ERROR;
804 1         38 ok(-e $s, 'symlink not deleted');
805 1         777 ok(-e $srcdir, 'symlink target dir is not deleted');
806 1         1044 ok(!-e $srcfile, 'file through symlink is deleted');
807 1         1010 return;
808 3     3   1602 }
  3         5  
  3         12  
809              
810             =item symlink_deep(), introduced in v0.06
811              
812             =cut
813              
814             sub symlink_deep : Test(17) : Introduced('0.06') : Features('symlink') {
815 1         2 my ($self) = @_;
816             # follow through a chain of non-looping symlinks
817 1         4 my $target = 'symlink_target';
818 1         7 my $srcdir = $self->_file("/$target");
819 1         6 my $srcfile = $self->_file("/$target/file.txt");
820             # 5 is the limit for Linux. Beyond that, we get ELOOP: Too many levels of symbolic links
821             # Other platforms seem to tolerate higher numbers...
822 1         5 my @s = map {$self->_file("/symlink_$_")} 1 .. 5; ## no critic(ValuesAndExpressions::ProhibitMagicNumbers)
  5         17  
823              
824 1 50       74 mkdir $srcdir or die $OS_ERROR;
825 1         3 my $content = 'content';
826 1         7 $self->_write_file($srcfile, $content);
827 1         78 ok(-e $srcfile, 'symlink source exists');
828 1         483 ok(-f $srcfile, 'symlink source is a file');
829              
830 1 50       373 ok((symlink $target, $s[0]), 'created symlink') or die $OS_ERROR;
831 1         450 for my $i (1..$#s) {
832 4 50       1295 ok((symlink 'symlink_' . $i, $s[$i]), 'created symlink') or die $OS_ERROR;
833             }
834 1         6361 my $symfile = $self->_file('/symlink_'.@s.'/file.txt');
835 1         105 ok(-e $symfile, 'file exists'); # or die 'no symlinked file, cannot continue';
836 1         1626 ok(!-l $symfile, 'file is not a symlink');
837 1         591 ok(-f $symfile, 'file is a file');
838 1         723 ok(!-d $symfile, 'file is not a dir');
839 1         1026 is(-s $symfile, length $content, 'size of file though symlink size is size of content');
840 1         1071 is($self->_read_file($symfile), $content, 'read file through newly created symlink');
841 1 50       1163 unlink $symfile or die $OS_ERROR;
842 1         35 ok(-e $s[0], 'symlink not deleted');
843 1         1405 ok(-e $s[-1], 'symlink not deleted');
844 1         1286 ok(-e $srcdir, 'symlink target dir is not deleted');
845 1         641 ok(!-e $srcfile, 'file through symlink is deleted');
846 1         715 return;
847 3     3   2016 }
  3         8  
  3         13  
848              
849             =item symlink_loop(), introduced in v0.06
850              
851             =cut
852              
853             sub symlink_loop : Test(2) : Introduced('0.06') : Features('symlink') {
854 1         3 my ($self) = @_;
855 1         4 my $target = 'symlink_target';
856 1         6 my $s = $self->_file("/$target");
857 1 50       59 ok((symlink $target, $s), 'created symlink') or die $OS_ERROR;
858 1         2682 eval {
859 1 50       644 open my $fh, '<', $s or die $OS_ERROR;
860 0         0 close $fh; ## no critic(InputOutput::RequireCheckedClose)
861             };
862 1         11 $self->_is_errno($EVAL_ERROR, $OS_ERROR, [ELOOP()], 'detected symlink loop');
863            
864 1         428 return;
865 3     3   1307 }
  3         7  
  3         10  
866              
867             =item truncate_file(), introduced in v0.06
868              
869             =cut
870              
871             sub truncate_file : Test(7) : Introduced('0.06') {
872 1         4 my ($self) = @_;
873 1         7 my $f = $self->_file(q{/truncate.txt});
874 1         3 my $content = 'content';
875              
876             ## no critic(ValuesAndExpressions::ProhibitMagicNumbers)
877 1         7 $self->_write_file($f, $content);
878 1         57 is(-s $f, length $content, 'wrote test file');
879 1 50       783 ok((truncate $f, 4), 'truncate to 4 bytes') or die $OS_ERROR;
880 1         556 is(-s $f, 4, 'correct size');
881 1 50       801 ok((truncate $f, 0), 'truncate to 0 bytes') or die $OS_ERROR;
882 1         570 is(-s $f, 0, 'correct size');
883 1 50       608 ok((truncate $f, 0), 'truncate to 0 bytes') or die $OS_ERROR;
884 1         584 is(-s $f, 0, 'correct size');
885 1         623 return;
886 3     3   1315 }
  3         6  
  3         14  
887              
888             =item truncate_no_file(), introduced in v0.06
889              
890             =cut
891              
892             sub truncate_no_file : Test(1) : Introduced('0.06') {
893 1         2 my ($self) = @_;
894 1         8 my $f = $self->_file(q{/truncate.txt});
895 1         4 eval {
896 1 50       44 truncate $f, 0 or die $OS_ERROR;
897             };
898 1         7 $self->_is_errno($EVAL_ERROR, $OS_ERROR, [ENOENT()], 'truncate non-existent file');
899 1         328 return;
900 3     3   912 }
  3         5  
  3         16  
901              
902             =item truncate_file_no_dir(), introduced in v0.06
903              
904             =cut
905              
906             sub truncate_file_no_dir : Test(1) : Introduced('0.06') {
907 1         4 my ($self) = @_;
908 1         5 my $pseudo_dir = $self->_file(q{/dir});
909 1         5 my $f = $self->_file(q{/dir/truncate.txt});
910 1         6 $self->_write_file($pseudo_dir, 'foo');
911 1         2 eval {
912 1 50       40 truncate $f, 0 or die $OS_ERROR;
913             };
914             # man 2 truncate says "[ENOTDIR] A component of the path prefix is not a directory."
915             # MSWin32 says ENOENT
916 1         9 $self->_is_errno($EVAL_ERROR, $OS_ERROR, [ENOTDIR(), ENOENT()], 'truncate file in non-existent directory');
917 1         375 return;
918 3     3   1063 }
  3         6  
  3         17  
919              
920             =item truncate_dir(), introduced in v0.06
921              
922             =cut
923              
924             sub truncate_dir : Test(1) : Introduced('0.06') {
925 1         5 my ($self) = @_;
926 1         8 my $d = $self->_file(q{/truncate_dir});
927 1 50       102 mkdir $d or die $OS_ERROR;
928 1         3 eval {
929 1 50       245 truncate $d, 0 or die $OS_ERROR;
930             };
931             # man 2 truncate says "[EISDIR] The named file is a directory."
932             # MSWin32 says EACCES
933 1         37 $self->_is_errno($EVAL_ERROR, $OS_ERROR, [EISDIR(), EACCES()], 'truncate dir');
934 1         549 return;
935 3     3   1014 }
  3         4  
  3         13  
936              
937             =item time_mtime_create(), introduced in v0.06
938              
939             =cut
940              
941             sub time_mtime_create : Test(2) : Introduced('0.06') : Features('time/mtime') {
942 1         3 my ($self) = @_;
943 1         5 my $f = $self->_file(q{/file.txt});
944              
945 1         4 my $before = time;
946 1         4 $self->_write_file($f);
947 1         2 my $after = time;
948              
949 1         23 my ($mtime) = (stat $f)[9]; ## no critic(ValuesAndExpressions::ProhibitMagicNumbers)
950 1 50       7 cmp_ok($mtime, q{>=}, $before - $TIME_LENIENCE, 'mtime vs. before time')
951             or diag 'Is your clock out of synch?';
952 1         349 cmp_ok($mtime, q{<=}, $after + $TIME_LENIENCE, 'mtime vs. after time');
953 1         1558 return;
954 3     3   995 }
  3         8  
  3         12  
955              
956             =item time_ctime_create(), introduced in v0.06
957              
958             =cut
959              
960             sub time_ctime_create : Test(2) : Introduced('0.06') : Features('time/ctime') {
961 1         3 my ($self) = @_;
962 1         7 my $f = $self->_file(q{/file.txt});
963              
964             # sleep is needed in case of network filesystem time synch errors
965 1         14 my $before = time;
966 1         7 $self->_write_file($f);
967 1         3 my $after = time;
968              
969 1         28 my ($ctime) = (stat $f)[10]; ## no critic(ValuesAndExpressions::ProhibitMagicNumbers)
970 1 50       8 cmp_ok($ctime, q{>=}, $before - $TIME_LENIENCE, 'ctime vs. before time')
971             or diag 'Is your clock out of synch?';
972 1         388 cmp_ok($ctime, q{<=}, $after + $TIME_LENIENCE, 'ctime vs. after time');
973 1         329 return;
974 3     3   1153 }
  3         7  
  3         14  
975              
976             =item time_mtime_set(), introduced in v0.06
977              
978             =cut
979              
980             sub time_mtime_set : Test(1) : Introduced('0.06') : Features('time/mtime') {
981 1         3 my ($self) = @_;
982 1         7 my $f = $self->_file(q{/file.txt});
983              
984 1         5 $self->_write_file($f);
985              
986             ## no critic(ValuesAndExpressions::ProhibitMagicNumbers)
987 1         23 my ($old_atime, $old_mtime) = (stat $f)[8,9];
988 1 50       49 utime $old_atime, $old_mtime - 100, $f or die $OS_ERROR;
989 1         20 my ($new_atime, $new_mtime) = (stat $f)[8,9];
990 1         7 is($new_mtime, $old_mtime - 100, 'changed mtime');
991 1         1866 return;
992 3     3   1041 }
  3         6  
  3         11  
993              
994             =item time_atime_set(), introduced in v0.06
995              
996             =cut
997              
998             sub time_atime_set : Test(1) : Introduced('0.06') : Features('time/atime') {
999 0         0 my ($self) = @_;
1000 0         0 my $f = $self->_file(q{/file.txt});
1001              
1002 0         0 $self->_write_file($f);
1003              
1004             ## no critic(ValuesAndExpressions::ProhibitMagicNumbers)
1005 0         0 my ($old_atime, $old_mtime) = (stat $f)[8,9];
1006 0 0       0 utime $old_atime - 100, $old_mtime, $f or die $OS_ERROR;
1007 0         0 my ($new_atime, $new_mtime) = (stat $f)[8,9];
1008 0         0 is($new_atime, $old_atime - 100, 'changed atime');
1009 0         0 return;
1010 3     3   1119 }
  3         6  
  3         12  
1011              
1012             =item xattr_list(), introduced in v0.02
1013              
1014             =cut
1015              
1016             sub xattr_list : Test(1) : Introduced('0.02') : Features('xattr') {
1017 0         0 my ($self) = @_;
1018 0         0 my $f = $self->_file(q{/});
1019 0         0 my @attrs = File::ExtAttr::listfattr($f);
1020 0   0     0 ok(@attrs == 0 || defined $attrs[0], 'got xattr list');
1021 0         0 return;
1022 3     3   1043 }
  3         6  
  3         52  
1023              
1024             =item xattr_set(), introduced in v0.02
1025              
1026             =cut
1027              
1028             sub xattr_set : Test(9) : Introduced('0.02') : Features('xattr') {
1029 0         0 my ($self) = @_;
1030              
1031 0         0 my $f = $self->_file('/foo');
1032 0         0 $self->_write_file($f);
1033              
1034 0         0 my $xattr_key = 'org.cpan.cdolan';
1035 0         0 my $xattr_value = 'test';
1036 0         0 my $xattr_replace = 'test2';
1037              
1038             # just in case, clean up. This fails if the value is '0' but that should never happen!
1039 0 0       0 if (File::ExtAttr::getfattr($f, $xattr_key)) {
1040 0         0 File::ExtAttr::delfattr($f, $xattr_key);
1041             }
1042             {
1043             # File::ExtAttr doesn't look at $^W or 'no warnings'. Grr...
1044 0         0 local $SIG{__WARN__} = sub {};
  0         0  
  0         0  
1045 0         0 ok(!File::ExtAttr::setfattr($f, $xattr_key, $xattr_value, {replace => 1}), 'cannot replace missing xattr');
1046             }
1047 0         0 ok(File::ExtAttr::setfattr($f, $xattr_key, $xattr_value, {create => 1}), 'create xattr');
1048 0         0 is(File::ExtAttr::getfattr($f, $xattr_key), $xattr_value, 'get xattr');
1049 0         0 ok((any {$xattr_key eq $_} File::ExtAttr::listfattr($f)), 'list xattr');
  0         0  
1050             {
1051 0         0 local $SIG{__WARN__} = sub {};
  0         0  
  0         0  
1052 0         0 ok(!File::ExtAttr::setfattr($f, $xattr_key, $xattr_value, {create => 1}), 'cannot create existing xattr');
1053             }
1054 0         0 ok(File::ExtAttr::setfattr($f, $xattr_key, $xattr_replace, {replace => 1}), 'replace xattr');
1055 0         0 is(File::ExtAttr::getfattr($f, $xattr_key), $xattr_replace, 'get xattr');
1056 0         0 ok(File::ExtAttr::delfattr($f, $xattr_key), 'delete xattr');
1057             # Some implementations return undef, some return q{}
1058 0         0 my $get = File::ExtAttr::getfattr($f, $xattr_key);
1059 0   0     0 ok(!defined $get || q{} eq $get, 'xattr deleted');
1060              
1061 0         0 unlink $f;
1062 0         0 return;
1063 3     3   1930 }
  3         6  
  3         18  
1064              
1065             =item rename_file(), introduced in v0.08
1066              
1067             =cut
1068              
1069             sub rename_file : Test(4) : Introduced('0.08') {
1070 1         3 my ($self) = @_;
1071 1         6 my $src = $self->_file('/rename_src');
1072 1         5 my $dest = $self->_file('/rename_dest');
1073 1         4 my $content = 'content';
1074 1         5 $self->_write_file($src, $content);
1075 1         71 ok((rename $src, $dest), 'rename');
1076 1         2035 ok(-e $dest, 'dest exists');
1077 1         970 ok(!-e $src, 'src no longer exists');
1078 1         299 is($self->_read_file($dest), $content, 'read dest');
1079 1         303 return;
1080 3     3   1293 }
  3         6  
  3         13  
1081              
1082             =item rename_file_exists(), introduced in v0.08
1083              
1084             =cut
1085              
1086             sub rename_file_exists : Test(4) : Introduced('0.08') {
1087 1         2 my ($self) = @_;
1088 1         7 my $src = $self->_file('/rename_src');
1089 1         6 my $dest = $self->_file('/rename_dest');
1090 1         4 my $content = 'content';
1091 1         7 $self->_write_file($src, $content);
1092 1         3 $self->_write_file($dest);
1093 1         81 ok((rename $src, $dest), 'rename');
1094 1         593 ok(-e $dest, 'dest exists');
1095 1         575 ok(!-e $src, 'src no longer exists');
1096 1         550 is($self->_read_file($dest), $content, 'read dest');
1097 1         677 return;
1098 3     3   1001 }
  3         4  
  3         14  
1099              
1100             =item rename_file_self(), introduced in v0.08
1101              
1102             =cut
1103              
1104             sub rename_file_self : Test(4) : Introduced('0.08') {
1105 1         3 my ($self) = @_;
1106 1         4 my $src = $self->_file('/rename_src');
1107 1         3 my $dest = $src;
1108 1         4 my $content = 'content';
1109 1         6 $self->_write_file($src, $content);
1110 1         38 ok((rename $src, $dest), 'rename');
1111 1         645 ok(-e $dest, 'dest exists');
1112 1         339 ok(-e $src, 'src still exists');
1113 1         283 is($self->_read_file($dest), $content, 'read dest');
1114 1         289 return;
1115 3     3   965 }
  3         5  
  3         23  
1116              
1117             =item rename_file_subdir(), introduced in v0.08
1118              
1119             =cut
1120              
1121             sub rename_file_subdir : Test(4) : Introduced('0.08') {
1122 1         3 my ($self) = @_;
1123 1         5 my $srcdir = $self->_file('/rename_srcdir');
1124 1         5 my $src = $self->_file('/rename_srcdir/rename_src');
1125 1         4 my $destdir = $self->_file('/rename_destdir');
1126 1         3 my $dest = $self->_file('/rename_destdir/rename_dest');
1127 1 50       75 mkdir $srcdir or die $OS_ERROR;
1128 1 50       59 mkdir $destdir or die $OS_ERROR;
1129 1         3 my $content = 'content';
1130 1         5 $self->_write_file($src, $content);
1131 1         71 ok((rename $src, $dest), 'rename');
1132 1         366 ok(-e $dest, 'dest exists');
1133 1         276 ok(!-e $src, 'src no longer exists');
1134 1         358 is($self->_read_file($dest), $content, 'read dest');
1135 1         284 return;
1136 3     3   1262 }
  3         13  
  3         14  
1137              
1138             =item rename_file_missing_src(), introduced in v0.08
1139              
1140             =cut
1141              
1142             sub rename_file_missing_src : Test(1) : Introduced('0.08') {
1143 1         160 my ($self) = @_;
1144 1         6 my $src = $self->_file('/rename_src');
1145 1         5 my $dest = $self->_file('/rename_dest');
1146 1         3 eval {
1147 1 50       53 rename $src, $dest or die $OS_ERROR;
1148             };
1149 1         6 $self->_is_errno($EVAL_ERROR, $OS_ERROR, [ENOENT()], 'src file is missing');
1150 1         326 return;
1151 3     3   1098 }
  3         12  
  3         14  
1152              
1153             =item rename_file_missing_srcdir(), introduced in v0.08
1154              
1155             =cut
1156              
1157             sub rename_file_missing_srcdir : Test(1) : Introduced('0.08') {
1158 1         3 my ($self) = @_;
1159 1         4 my $srcdir = $self->_file('/rename_srcdir');
1160 1         5 my $src = $self->_file('/rename_srcdir/rename_src');
1161 1         5 my $dest = $self->_file('/rename_dest');
1162 1         4 my $content = 'content';
1163 1         2 eval {
1164 1 50       39 rename $src, $dest or die $OS_ERROR;
1165             };
1166 1         6 $self->_is_errno($EVAL_ERROR, $OS_ERROR, [ENOENT()], 'src dir is missing');
1167 1         508 return;
1168 3     3   1019 }
  3         6  
  3         20  
1169              
1170             =item rename_file_missing_destdir(), introduced in v0.08
1171              
1172             =cut
1173              
1174             sub rename_file_missing_destdir : Test(1) : Introduced('0.08') {
1175 1         3 my ($self) = @_;
1176 1         6 my $src = $self->_file('/rename_src');
1177 1         6 my $destdir = $self->_file('/rename_destdir');
1178 1         4 my $dest = $self->_file('/rename_destdir/rename_dest');
1179 1         4 my $content = 'content';
1180 1         7 $self->_write_file($src, $content);
1181 1         2 eval {
1182 1 50       57 rename $src, $dest or die $OS_ERROR;
1183             };
1184 1         9 $self->_is_errno($EVAL_ERROR, $OS_ERROR, [ENOENT()], 'dest dir is missing');
1185 1         371 return;
1186 3     3   1135 }
  3         5  
  3         27  
1187              
1188             =item rename_dir(), introduced in v0.08
1189              
1190             =cut
1191              
1192             sub rename_dir : Test(6) : Introduced('0.08') {
1193 1         2 my ($self) = @_;
1194 1         4 my $src = $self->_file('/rename_src');
1195 1         5 my $srcfile = $self->_file('/rename_src/file.txt');
1196 1         5 my $dest = $self->_file('/rename_dest');
1197 1         4 my $destfile = $self->_file('/rename_dest/file.txt');
1198 1 50       90 mkdir $src or die $OS_ERROR;
1199 1         2 my $content = 'content';
1200 1         5 $self->_write_file($srcfile, $content);
1201 1         75 ok((rename $src, $dest), 'rename');
1202 1         541 ok(-e $dest, 'dest exists');
1203 1         456 ok(-e $destfile, 'dest file exists');
1204 1         320 ok(!-e $src, 'src no longer exists');
1205 1         290 is_deeply([sort $self->_read_dir($dest)], [qw(. .. file.txt)], 'read dest');
1206 1         678 is($self->_read_file($destfile), $content, 'read dest');
1207 1         335 return;
1208 3     3   1268 }
  3         5  
  3         14  
1209              
1210             =item rename_dir_exists(), introduced in v0.08
1211              
1212             =cut
1213              
1214             sub rename_dir_exists : Test(6) : Introduced('0.08') {
1215 1         7 my ($self) = @_;
1216 1         5 my $src = $self->_file('/rename_src');
1217 1         4 my $srcfile = $self->_file('/rename_src/file.txt');
1218 1         5 my $dest = $self->_file('/rename_dest');
1219 1         4 my $destfile = $self->_file('/rename_dest/file.txt');
1220 1         3 my $content = 'content';
1221 1 50       80 mkdir $src or die $OS_ERROR;
1222 1 50       57 mkdir $dest or die $OS_ERROR;
1223 1         6 $self->_write_file($srcfile, $content);
1224 1 50       7 if ($OSNAME eq 'MSWin32') {
1225             # return the skip message
1226 0         0 return 'Cannot overwrite directories via rename on Windows';
1227             }
1228 1         139 ok((rename $src, $dest), 'rename');
1229 1         351 ok(-e $dest, 'dest exists');
1230 1         277 ok(-e $destfile, 'dest file exists');
1231 1         278 ok(!-e $src, 'src no longer exists');
1232 1         256 is_deeply([sort $self->_read_dir($dest)], [qw(. .. file.txt)], 'read dest');
1233 1         556 is($self->_read_file($destfile), $content, 'read dest');
1234 1         334 return;
1235 3     3   1311 }
  3         7  
  3         20  
1236              
1237             =item rename_dir_notempty(), introduced in v0.08
1238              
1239             =cut
1240              
1241             sub rename_dir_notempty : Test(1) : Introduced('0.08') {
1242 1         2 my ($self) = @_;
1243 1         6 my $src = $self->_file('/rename_src');
1244 1         6 my $dest = $self->_file('/rename_dest');
1245 1         4 my $destfile = $self->_file('/rename_dest/file.txt');
1246 1         4 my $content = 'content';
1247 1 50       82 mkdir $src or die $OS_ERROR;
1248 1 50       71 mkdir $dest or die $OS_ERROR;
1249 1         5 $self->_write_file($destfile, $content);
1250 1         3 eval {
1251 1 50       70 rename $src, $dest or die $OS_ERROR;
1252             };
1253             # man 2 rename says "[ENOTEMPTY] To is a directory and is not empty."
1254             # MSWin32 says EACCES
1255             # Solaris says EEXIST
1256 1         8 $self->_is_errno($EVAL_ERROR, $OS_ERROR, [ENOTEMPTY(), EACCES(), EEXIST()], 'dest dir is not empty');
1257 1         362 return;
1258 3     3   1058 }
  3         6  
  3         14  
1259              
1260             =item rename_dir_self(), introduced in v0.08
1261              
1262             =cut
1263              
1264             sub rename_dir_self : Test(5) : Introduced('0.08') {
1265 1         3 my ($self) = @_;
1266 1         5 my $src = $self->_file('/rename_src');
1267 1         5 my $srcfile = $self->_file('/rename_src/file.txt');
1268 1         4 my $dest = $src;
1269 1         2 my $destfile = $srcfile;
1270 1 50       71 mkdir $src or die $OS_ERROR;
1271 1         3 my $content = 'content';
1272 1         5 $self->_write_file($srcfile, $content);
1273 1         33 ok((rename $src, $dest), 'rename');
1274 1         339 ok(-e $dest, 'dest exists');
1275 1         304 ok(-e $destfile, 'dest file exists');
1276 1         257 is_deeply([sort $self->_read_dir($dest)], [qw(. .. file.txt)], 'read dest');
1277 1         559 is($self->_read_file($destfile), $content, 'read dest');
1278 1         273 return;
1279 3     3   1089 }
  3         6  
  3         11  
1280              
1281             =item rename_dir_subdir(), introduced in v0.08
1282              
1283             =cut
1284              
1285             sub rename_dir_subdir : Test(6) : Introduced('0.08') {
1286 1         3 my ($self) = @_;
1287 1         5 my $srcdir = $self->_file('/rename_srcdir');
1288 1         5 my $src = $self->_file('/rename_srcdir/rename_src');
1289 1         7 my $srcfile = $self->_file('/rename_srcdir/rename_src/file.txt');
1290 1         5 my $destdir = $self->_file('/rename_destdir');
1291 1         5 my $dest = $self->_file('/rename_destdir/rename_dest');
1292 1         5 my $destfile = $self->_file('/rename_destdir/rename_dest/file.txt');
1293 1 50       76 mkdir $srcdir or die $OS_ERROR;
1294 1 50       62 mkdir $destdir or die $OS_ERROR;
1295 1 50       65 mkdir $src or die $OS_ERROR;
1296 1         3 my $content = 'content';
1297 1         5 $self->_write_file($srcfile, $content);
1298 1         80 ok((rename $src, $dest), 'rename');
1299 1         390 ok(-e $dest, 'dest exists');
1300 1         326 ok(-e $destfile, 'dest file exists');
1301 1         315 ok(!-e $src, 'src no longer exists');
1302 1         273 is_deeply([sort $self->_read_dir($dest)], [qw(. .. file.txt)], 'read dest');
1303 1         540 is($self->_read_file($destfile), $content, 'read dest');
1304 1         263 return;
1305 3     3   1485 }
  3         15  
  3         13  
1306              
1307             =item rename_mismatch_dir(), introduced in v0.08
1308              
1309             =cut
1310              
1311             sub rename_mismatch_dir : Test(1) : Introduced('0.08') {
1312 1         4 my ($self) = @_;
1313 1         5 my $src = $self->_file('/rename_src');
1314 1         7 my $dest = $self->_file('/rename_dest');
1315 1         4 my $content = 'content';
1316 1         5 $self->_write_file($src, $content);
1317 1 50       72 mkdir $dest or die $OS_ERROR;
1318 1         4 eval {
1319 1 50       49 rename $src, $dest or die $OS_ERROR;
1320             };
1321             # man 2 rename says "[EISDIR] 'to' is a directory, but 'from' is not a directory."
1322             # MSWin32 says EACCES
1323 1         9 $self->_is_errno($EVAL_ERROR, $OS_ERROR, [EISDIR(), EACCES()], 'dest is a directory');
1324 1         341 return;
1325 3     3   1096 }
  3         7  
  3         13  
1326              
1327             =item rename_mismatch_file(), introduced in v0.08
1328              
1329             =cut
1330              
1331             sub rename_mismatch_file : Test(1) : Introduced('0.08') {
1332 1         4 my ($self) = @_;
1333 1         5 my $src = $self->_file('/rename_srcdir');
1334 1         6 my $dest = $self->_file('/rename_destfile.txt');
1335 1 50       185 mkdir $src or die $OS_ERROR;
1336 1         2 my $content = 'content';
1337 1         7 $self->_write_file($dest, $content);
1338 1 50 33     14 if ($OSNAME eq 'MSWin32' || $OSNAME eq 'cygwin') {
1339             # return the skip message
1340 0         0 return 'Windows and Cygwin allow rename(, ) instead of failing with ENOTDIR';
1341             }
1342 1         4 eval {
1343 1 50       123 rename $src, $dest or die $OS_ERROR;
1344             };
1345 1         8 $self->_is_errno($EVAL_ERROR, $OS_ERROR, [ENOTDIR()], 'dest is not a directory');
1346              
1347 1         388 return;
1348 3     3   1358 }
  3         8  
  3         13  
1349              
1350             =item rename_symlink(), introduced in v0.08
1351              
1352             =cut
1353              
1354             sub rename_symlink : Test(6) : Introduced('0.08') : Features('symlink') {
1355 1         13 my ($self) = @_;
1356 1         5 my $srcfile = $self->_file('/rename_srcfile.txt');
1357 1         5 my $src = $self->_file('/rename_src');
1358 1         4 my $dest = $self->_file('/rename_dest');
1359 1         3 my $content = 'content';
1360 1         5 $self->_write_file($srcfile, $content);
1361 1 50       62 symlink $srcfile, $src or die $OS_ERROR;
1362 1         68 ok((rename $src, $dest), 'rename');
1363 1         518 ok(-e $dest, 'dest exists');
1364 1         315 ok(-e $srcfile, 'source target file still exists');
1365 1         392 ok(!-e $src, 'src no longer exists');
1366 1         337 ok(-l $dest, 'dest is a symlink');
1367 1         257 is($self->_read_file($dest), $content, 'read dest');
1368 1         311 return;
1369 3     3   1276 }
  3         7  
  3         14  
1370              
1371             ######### helpers ########
1372              
1373              
1374             sub _is_errno {
1375 14     14   68 my ($self, $eval_error, $os_errno, $expected_errnos, $msg) = @_;
1376 14         124 my $num_errno = 0 + $os_errno;
1377 14         37 my $str_errno = "$os_errno";
1378 14 50 33 14   139 return pass($msg) if $eval_error && $num_errno && any {$_ == $num_errno} @{$expected_errnos};
  14   33     115  
  14         152  
1379 0         0 my $expected_str = join q{, }, map {strerror($_)} @{$expected_errnos};
  0         0  
  0         0  
1380 0 0       0 if (!$eval_error) {
  0 0       0  
1381 0         0 return fail("$msg; didn't throw expected exception");
1382             } elsif (1 == @{$expected_errnos}) {
1383 0         0 return is("$num_errno ($str_errno)", "$expected_errnos->[0] ($expected_str)", $msg);
1384             } else {
1385 0         0 return is("$num_errno ($str_errno)", "[@{$expected_errnos}] ($expected_str)", $msg);
  0         0  
1386             }
1387             }
1388              
1389             sub _file {
1390 81     81   265 my ($self, $path) = @_;
1391 81 50       480 $path =~ s{\A /}{}xms or croak 'test paths must be absolute';
1392             # Change path to proper OS format
1393 81         1099 return File::Spec->catfile($self->{tempdir}, split m{/}xms, $path);
1394             }
1395              
1396             sub _write_file {
1397 29     29   90 my ($self, $f, @content) = @_;
1398 29 100       3535 open my $fh, '>', $f or die $OS_ERROR;
1399 28         86 binmode $fh;
1400 28         65 for my $content (@content) {
1401 23 50       40 print {$fh} $content or die $OS_ERROR;
  23         422  
1402             }
1403 28 50       1506 close $fh or die $OS_ERROR;
1404 28         423 return;
1405             }
1406              
1407             sub _append_file {
1408 1     1   18 my ($self, $f, @content) = @_;
1409 1 50       54 open my $fh, '>>', $f or die $OS_ERROR;
1410 1         3 binmode $fh;
1411 1         4 for my $content (@content) {
1412 1 50       2 print {$fh} $content or die $OS_ERROR;
  1         9  
1413             }
1414 1 50       27 close $fh or die $OS_ERROR;
1415 1         6 return;
1416             }
1417              
1418             sub _read_file {
1419 14     14   39 my ($self, $f) = @_;
1420 14 50       866 open my $fh, '<', $f or return;
1421 14         42 binmode $fh;
1422 14         22 my $content = do { local $INPUT_RECORD_SEPARATOR = undef; <$fh> };
  14         73  
  14         392  
1423 14 50       402 close $fh or return;
1424 14         126 return $content;
1425             }
1426              
1427             sub _read_file_die {
1428 1     1   5 my ($self, $f) = @_;
1429 1 50       68 open my $fh, '<', $f or die $OS_ERROR;
1430 0         0 binmode $fh;
1431 0         0 my $content = do { local $INPUT_RECORD_SEPARATOR = undef; <$fh> };
  0         0  
  0         0  
1432 0 0       0 close $fh or die $OS_ERROR;
1433 0         0 return $content;
1434             }
1435              
1436             sub _read_dir {
1437 71     71   132 my ($self, $f) = @_;
1438 71 50       3082 opendir my $fh, $f or return;
1439 71         2314 my @content = readdir $fh;
1440 71 50       976 closedir $fh or return;
1441 71         701 return @content;
1442             }
1443              
1444             sub _read_dir_die {
1445 1     1   3 my ($self, $f) = @_;
1446 1 50       57 opendir my $fh, $f or die $OS_ERROR;
1447 0           my @content = readdir $fh;
1448 0 0         closedir $fh or die $OS_ERROR;
1449 0           return @content;
1450             }
1451              
1452             1;
1453              
1454             __END__