diff --git a/src/main/perl/Path.pm b/src/main/perl/Path.pm index aacbdc10..fb449135 100644 --- a/src/main/perl/Path.pm +++ b/src/main/perl/Path.pm @@ -28,6 +28,7 @@ Readonly::Hash my %CLEANUP_DISPATCH => { # Use dispatch table instead of non-strict function by variable call Readonly::Hash my %LC_CHECK_DISPATCH => { directory => \&LC::Check::directory, + link => \&LC::Check::link, status => \&LC::Check::status, }; @@ -356,6 +357,9 @@ sub any_exists Test if C is a symlink. +Returns true as long as C is a symlink, even though the +symlink target doesn't exist. + =cut sub is_symlink @@ -562,6 +566,118 @@ sub directory } +=item _make_link + +This method is mainly a wrapper over C +returning the standard C return values. Every option +supported by C is supported. C +flag is handled by C and C option +is honored (overrides C if true). One important +different is the order of the arguments: C +and the methods based on it are following the Perl C +(and Shell C) argument order. + +This in internal method, not supposed to be called directly. +Either call C or C public methods instead. + +=cut + +sub _make_link +{ + my ($self, $target, $link_path, %opts) = @_; + my $link_type = $opts{hard} ? "hardlink" : "symlink"; + + $link_path = $self->_untaint_path($link_path, $link_type) || return; + $target = $self->_untaint_path($target, $link_type) || return; + + $self->_reset_exception_fail($link_type); + + my $status = $self->LC_Check('link', [$link_path, $target], \%opts); + + if ( defined($status) ) { + return ($status ? CHANGED : SUCCESS); + } else { + return; + } +} + +=item hardlink + +Create a hardlink C whose target is C. + +On failure, returns undef and sets the fail attribute. +If C exists and is a file, it is updated. +C must exist (C flag available in symlink() +is ignored for hardlinks) and it must reside in the same +file system as C. Also, if C is a +relative path, C parent directory is prepended to it +(C can be a relative path): this way the API +produces the same result for a symlink and a hardlink. +C parent directory is created if it doesn't exist. + +Returns SUCCESS on sucess if the hardlink already existed +with the same target, CHANGED if the hardlink was created +or updated, undef otherwise. + +This method relies on C<_make_link> method to do the real work, +after enforcing the option saying that it is a hardlink. + +=cut + +sub hardlink +{ + my ($self, $target, $link_path, %opts) = @_; + + # Option passed to LC::Check::link to indicate a hardlink + $opts{hard} = 1; + + return $self->_make_link($target, $link_path, %opts); +} + + +=item symlink + +Create a symlink C whose target is C. + +Returns undef and sets the fail attribute if C +already exists and is not a symlink, except if this is a file and option C is +defined and true. If C exists and is a symlink, it is updated. +By default, the target is not required to exist. If you want to +ensure that it exists, define option C to true. +Both C and C can be relative paths and are +kept relative in this case. C parent directory +is created if it doesn't exist. + +Returns SUCCESS on sucess if the symlink already existed +with the same target, CHANGED if the symlink was created +or updated, undef otherwise. + +This method relies on C<_make_link> method to do the real work, +after enforcing the option saying that it is a symlink. + +=cut + +sub symlink +{ + my ($self, $target, $link_path, %opts) = @_; + + # Option passed to LC::Check::link to indicate a symlink + $opts{hard} = 0; + + # LC::Check::symlink() expects an option 'nocheck' but CAF::Path::symlink exposes + # an option 'check', as the default in CAF::Path::symlink is not to check the + # target existence. Convert it to 'nocheck'. + if ( defined($opts{check}) ) { + $opts{nocheck} = ! $opts{check}; + delete $opts{check}; + } else { + $opts{nocheck} = 1; + } + + return $self->_make_link($target, $link_path, %opts); +} + + =item status Set the path stat options: C, C, C and/or C. diff --git a/src/test/perl/path.t b/src/test/perl/path.t index a18c9121..d537429c 100644 --- a/src/test/perl/path.t +++ b/src/test/perl/path.t @@ -11,6 +11,7 @@ use LC::Exception qw (throw_error); use Test::More; use Test::MockModule; +use Cwd; use CAF::Object qw(SUCCESS CHANGED); use CAF::Path; @@ -81,6 +82,11 @@ ok(! $mc->_get_noaction(1), "_get_noaction returns false with CAF::Object::NoAct my $exception_reset = 0; +# init_exception() and verify_exception() functions work in pair. They allow to register a message +# in 'fail' attribute at the beginning of a test section and to verify if new (unexpected) exceptions +# where raised during the test section. To reset the 'fail' attribute after verify_exception(), +# call _reset_exception_fail(). init_exception() implicitely resets the 'fail' attribute and also +# reset to 0 the count of calls to _reset_exception_fail(). sub init_exception { my ($msg) = @_; @@ -133,6 +139,21 @@ $mock->mock('_reset_exception_fail', sub { return &$init(@_); }); +# Mocked symlink() and hardlink() to count calls +my $symlink_call_count = 0; +my $hardlink_call_count = 0; + +$mock->mock('symlink', sub { + $symlink_call_count += 1; + my $symlink_orig = $mock->original('symlink'); + return &$symlink_orig(@_); +}); + +$mock->mock('hardlink', sub { + $hardlink_call_count += 1; + my $hardlink_orig = $mock->original('hardlink'); + return &$hardlink_orig(@_); +}); =head2 _function_catch @@ -277,11 +298,12 @@ $mc->{fail} = undef; is($mc->_untaint_path("abc", "ok"), "abc", "untaint ok"); ok(! defined($mc->{fail}), "no fail attribute set with ok path"); -=head2 directory/file/any exists + +=head2 directory/file/any exists on files and directories =cut -init_exception("existence tests"); +init_exception("existence tests (file/directory)"); # Tests without NoAction $CAF::Object::NoAction = 0; @@ -309,12 +331,113 @@ ok($mc->any_exists($basetestfile), "any_exists true on created file"); ok($mc->file_exists($basetestfile), "file_exists true on created file"); ok(! $mc->is_symlink($basetestfile), "is_symlink false on created file"); -# Test (broken) symlink and _exsists methods +# noreset=1 +verify_exception("existence tests (file/directory)", undef, 0, 1); +ok($mc->_reset_exception_fail(), "_reset_exception_fail after existence tests (file/directory)"); + + +=head2 symlink/hardlink creation/update/test + +=cut + +# Test symlink creation + +# Function to do the ok()/is() pair, taking into account NoAction flag +sub check_symlink { + my ($mc, $target, $link_path, $expected_status) = @_; + my $noaction = $mc->_get_noaction(); -ok(symlink("really_really_missing", $brokenlink), "broken symlink created"); -makefile("$basetest/tgtdir/tgtfile"); -ok(symlink("tgtdir", $dirlink), "directory symlink created"); -ok(symlink("tgtdir/tgtfile", $filelink), "file symlink created"); + my $ok_msg; + if ( $noaction ) { + $ok_msg = "$link_path symlink not created (NoAction set)"; + } else { + $ok_msg = "$link_path is " . ($expected_status == CHANGED ? "" : "already ") . "a symlink"; + } + my $target_msg = "$link_path symlink has the expected " . ($expected_status == CHANGED ? "changed " : "") . "target ($target)"; + + # Test if symlink was created or not according to NoAction flag + my $ok_condition; + if ( $noaction ) { + $ok_condition = ! $mc->any_exists($link_path); + } else { + $ok_condition = $mc->is_symlink($link_path); + } + ok($ok_condition, $ok_msg); + + is(readlink($link_path), $target, $target_msg) unless $noaction; +}; + +init_exception("symlink tests"); + +rmtree ($basetest) if -d $basetest; +my $target_directory = "tgtdir"; +my $target_file1 = "tgtfile1"; +my $target_file2 = "$basetest/$target_directory/tgtfile2"; +makefile("$basetest/$target_file1"); +makefile($target_file2); +my %opts; + +# Symlink creations +for $CAF::Object::NoAction (1,0) { + is($mc->symlink($target_directory, $dirlink), CHANGED, "directory symlink created"); + check_symlink($mc, $target_directory, $dirlink, CHANGED); + is($mc->symlink($target_file2, $filelink), CHANGED, "file symlink created"); + check_symlink($mc, $target_file2, $filelink, CHANGED); +} + +# Valid symlink updates +# The following tests make no sens if NoAction is true (require the previous creations to occur) +is($mc->symlink($target_file2, $filelink), SUCCESS, "file symlink already exists: nothing done"); +check_symlink($mc, $target_file2, $filelink, SUCCESS); +is($mc->symlink($target_file1, $filelink), CHANGED, "file symlink updated"); +check_symlink($mc, $target_file1, $filelink, CHANGED); +ok(! readlink($target_file1), "$target_file1 exists and is a file"); +my $link_status = $mc->symlink($target_file1, $target_file2); +ok(! defined($link_status), "symlink failed: existing file not replaced by a symlink"); +is($mc->{fail}, + "*** cannot symlink $target_file2: it is not an existing symlink", + "fail attribute set after symlink failure (existing file not replaced by a symlink)"); +ok($mc->file_exists($target_file2) && ! $mc->is_symlink($target_file2), "File $target_file2 has not be replaced by a symlink"); +$opts{force} = 1; +$CAF::Object::NoAction = 1; +is($mc->symlink($target_file1, $target_file2, %opts), CHANGED, "existing file replaced by a symlink (force option set)"); +ok($mc->file_exists($target_file2), "File $target_file2 not replaced by a symlink with 'force' option (NoAction set)"); +$CAF::Object::NoAction = 0; +is($mc->symlink($target_file1, $target_file2, %opts), CHANGED, "existing file replaced by a symlink (force option set)"); +check_symlink($mc, $target_file1, $target_file2, CHANGED); + +# Invalid symlink updates +my $test_directory = "$basetest/$target_directory"; +$link_status = $mc->symlink($target_file1, "$test_directory", %opts); +ok (! defined($link_status), "directory not replaced by a symlink (force option set)"); +is($mc->{fail}, + "*** cannot symlink $test_directory: it is not an existing symlink", + "fail attribute set after symlink failure (existing directory not replaced by a symlink)"); +ok($mc->directory_exists($test_directory) && ! $mc->is_symlink($test_directory), + "Directory $test_directory has not be replaced by a symlink"); + +# Broken symlinks with and without 'check' option +$opts{check} = 1; +ok(! $mc->symlink("really_really_missing", $brokenlink, %opts), "broken symlink not created (target existence enforced)"); +ok(! -e $brokenlink && ! -l $brokenlink, "Broken link has not been created"); +$opts{check} = 0; +is($mc->symlink("really_missing", $brokenlink), CHANGED, "broken symlink created"); +ok($mc->is_symlink($brokenlink), "Broken link has been created (target check disabled by check=0)"); +is(readlink($brokenlink), "really_missing", "Broken link has the expected target by check=0"); +delete $opts{check}; +is($mc->symlink("really_really_missing", $brokenlink), CHANGED, "broken symlink updated"); +ok($mc->is_symlink($brokenlink), "Broken link has been updated (target check disabled by 'check' undefined)"); +is(readlink($brokenlink), "really_really_missing", "Broken link has the expected target by 'check' undefined"); + +# noreset=0 +verify_exception("symlink tests", "Failed to create symlink", $symlink_call_count * 2, 0); +ok($mc->_reset_exception_fail(), "_reset_exception_fail after symlink tests"); + + +# Test xxx_exists methods with symlinks +# Needs symlinks created in previous step (symlink creation/update) + +init_exception("existence tests (symlinks)"); ok(! $mc->directory_exists($brokenlink), "directory_exists false on brokenlink"); ok(! $mc->file_exists($brokenlink), "file_exists false on brokenlink"); @@ -331,16 +454,92 @@ ok($mc->file_exists($filelink), "file_exists true on filelink"); ok($mc->any_exists($filelink), "any_exists true on filelink"); ok($mc->is_symlink($filelink), "is_symlink true on filelink"); - # noreset=1 -verify_exception("existence tests do not reset exception/fail", "origfailure", 0, 1); -ok($mc->_reset_exception_fail(), "_reset_exception_fail after existence tests"); +verify_exception("existence tests (symlinks)", undef, 0, 1); +ok($mc->_reset_exception_fail(), "_reset_exception_fail after existence tests (symlinks)"); + + +# Test hardlink creation + +# Function to do the hardlink checks, taking into account NoAction flag +sub check_hardlink { + my ($mc, $target, $link_path, $expected_status) = @_; + my $noaction = $mc->_get_noaction(); + + my $ok_msg; + if ( $noaction ) { + $ok_msg = "$link_path hardlink not created (NoAction set)"; + } else { + $ok_msg = "$link_path is " . ($expected_status == CHANGED ? "" : "already ") . "a hardlink"; + } + my $target_msg = "$link_path hardlink has the expected " . ($expected_status == CHANGED ? "changed " : "") . "target ($target)"; + + my ($link_inode, $nlink) = (stat($link_path))[1, 3]; + my $target_inode = (stat($target))[1]; + + # Test if symlink was created or not according to NoAction flag + my $ok_condition; + if ( $noaction ) { + $ok_condition = ! $mc->any_exists($link_path); + }else { + $ok_condition = $nlink && ($nlink > 1); + } + ok($ok_condition, $ok_msg); + + ok($link_inode == $target_inode, $target_msg) unless $noaction; +}; + +init_exception("hardlink tests"); + +rmtree ($basetest) if -d $basetest; +my $cwd = cwd(); +my $hardlink1 = "$basetest/$target_directory/hardlink1"; +my $hardlink2 = "$basetest/$target_directory/hardlink2"; +# hardlink target must be an absolute path or the link path directory is prepended +$target_file1 = "$cwd/$basetest/tgtfile1"; +my $relative_target_file3 = "$basetest/$target_directory/tgtfile3"; +my $target_file3 = "$cwd/$relative_target_file3"; +makefile($target_file1); +makefile($target_file3); + +for $CAF::Object::NoAction (1,0) { + is($mc->hardlink($target_file1, $hardlink1), CHANGED, "hardlink created"); + check_hardlink($mc, $target_file1, $hardlink1, CHANGED); + if ( !$CAF::Object::NoAction ) { + is($mc->hardlink($target_file1, $hardlink1), SUCCESS, "hardlink already exists"); + check_hardlink($mc, $target_file1, $hardlink1, SUCCESS); + } + is($mc->hardlink($target_file3, $hardlink1), CHANGED, "hardlink updated"); + check_hardlink($mc, $target_file3, $hardlink1, CHANGED); +} + +# The following tests are not affected by NoAction flag +$link_status = $mc->hardlink("missing_file", $hardlink1); +ok(! $link_status, "hardlink not created (target has to exist)"); +is($mc->{fail}, + "*** invalid target (missing_file): stat($basetest/$target_directory/missing_file): No such file or directory", + "fail attribute set after hardlink error"); +$link_status = $mc->hardlink($target_file1, "$basetest/$target_directory"); +ok(! $link_status, "hardlink not created (do not replace an existing directory)"); +is($mc->{fail}, + "*** cannot hard link $basetest/$target_directory: it is a directory", + "fail attribute set after hardlink error (existing directory not replaced)"); +$link_status = $mc->hardlink($relative_target_file3, $hardlink2); +ok(! $link_status, "hardlink not created (relative target, link path directory prepended)"); +is($mc->{fail}, + "*** invalid target ($relative_target_file3): stat(". dirname($hardlink2) . "/$relative_target_file3): No such file or directory", + "fail attribute set after hardlink error (relative target, link path directory prepended)"); + +# noreset=0 +verify_exception("hardlink tests", "\\*\\*\\* invalid target", $hardlink_call_count * 2, 0); +ok($mc->_reset_exception_fail(), "_reset_exception_fail after hardlink tests"); =head2 directory =cut +rmtree ($basetest) if -d $basetest; $mc->{fail} = undef; ok(! defined $mc->directory("\0"), "failing untaint directory returns undef"); is($mc->{fail}, "Failed to untaint directory: path \0",