From dcccba655fe848564e961b3f285ce3a82d3ac73a Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Sat, 7 Mar 2020 14:07:28 +0300 Subject: Add more support for symlinks on Windows See mksymlink() for details of the symlinks support on Windows. --- tests/cpfile/driver.cxx | 134 ++++++++++++++------------ tests/dir-iterator/testscript | 100 ++++++++++++++------ tests/fdstream/driver.cxx | 38 +++++--- tests/link/driver.cxx | 93 ++++++++++++++---- tests/mventry/testscript | 51 +++++++++- tests/path-entry/driver.cxx | 212 +++++++++++++++++++++++++++++++++++++----- tests/path-entry/testscript | 175 ++++++++++++++++++++++++++++++++-- tests/wildcard/testscript | 52 +++++++---- 8 files changed, 676 insertions(+), 179 deletions(-) (limited to 'tests') diff --git a/tests/cpfile/driver.cxx b/tests/cpfile/driver.cxx index ae40b5f..c613b49 100644 --- a/tests/cpfile/driver.cxx +++ b/tests/cpfile/driver.cxx @@ -30,10 +30,7 @@ using namespace butl; static const char text1[] = "ABCDEF\nXYZ"; static const char text2[] = "12345\nDEF"; - -#ifndef _WIN32 static const char text3[] = "XAB\r\n9"; -#endif static string from_file (const path& f) @@ -135,72 +132,85 @@ main () assert (from_file (hlink) == text2); -#ifndef _WIN32 - - // Check that 'from' being a symbolic link is properly resolved. + // Note that on Windows regular file symlinks may not be supported (see + // mksymlink() for details), so the following tests are allowed to fail + // with ENOSYS on Windows. // - path fslink (td / path ("fslink")); - mksymlink (from, fslink); - - cpfile (fslink, to, cpflags::overwrite_content); - - // Make sure 'to' is not a symbolic link to 'from' and from_file() just - // follows it. - // - assert (try_rmfile (from) == rmfile_status::success); - assert (from_file (to) == text2); - - // Check that 'to' being a symbolic link is properly resolved. - // - path tslink (td / path ("tslink")); - mksymlink (to, tslink); - - to_file (from, text3); - cpfile (from, tslink, cpflags::overwrite_content); - assert (from_file (to) == text3); - - // Check that permissions are properly overwritten when 'to' is a symbolic - // link. - // - to_file (from, text1); - path_permissions (from, permissions::ru | permissions::xu); - - cpfile ( - from, tslink, cpflags::overwrite_content | cpflags::overwrite_permissions); - - assert (from_file (to) == text1); - assert (path_permissions (to) == path_permissions (from)); - - path_permissions (to, p); - path_permissions (from, p); - - // Check that no-overwrite file copy fails even if 'to' symlink points to - // non-existent file. - // - assert (try_rmfile (to) == rmfile_status::success); - try { - cpfile (from, tslink, cpflags::none); - assert (false); - } - catch (const system_error&) - { + // Check that 'from' being a symbolic link is properly resolved. + // + path fslink (td / path ("fslink")); + mksymlink (from, fslink); + + cpfile (fslink, to, cpflags::overwrite_content); + + // Make sure 'to' is not a symbolic link to 'from' and from_file() just + // follows it. + // + assert (try_rmfile (from) == rmfile_status::success); + assert (from_file (to) == text2); + + // Check that 'to' being a symbolic link is properly resolved. + // + path tslink (td / path ("tslink")); + mksymlink (to, tslink); + + to_file (from, text3); + cpfile (from, tslink, cpflags::overwrite_content); + assert (from_file (to) == text3); + + // Check that permissions are properly overwritten when 'to' is a symbolic + // link. + // + to_file (from, text1); + path_permissions (from, permissions::ru | permissions::xu); + + cpfile ( + from, tslink, cpflags::overwrite_content | cpflags::overwrite_permissions); + + assert (from_file (to) == text1); + assert (path_permissions (to) == path_permissions (from)); + + path_permissions (to, p); + path_permissions (from, p); + + // Check that no-overwrite file copy fails even if 'to' symlink points to + // non-existent file. + // + assert (try_rmfile (to) == rmfile_status::success); + + try + { + cpfile (from, tslink, cpflags::none); + assert (false); + } + catch (const system_error&) + { + } + + // Check that copy fail if 'from' symlink points to non-existent file. The + // std::system_error is thrown as cpfile() fails to obtain permissions for + // the 'from' symlink target. + // + try + { + cpfile (tslink, from, cpflags::none); + assert (false); + } + catch (const system_error&) + { + } } - - // Check that copy fail if 'from' symlink points to non-existent file. The - // std::system_error is thrown as cpfile() fails to obtain permissions for - // the 'from' symlink target. - // - try + catch (const system_error& e) { - cpfile (tslink, from, cpflags::none); +#ifndef _WIN32 assert (false); - } - catch (const system_error&) - { - } +#else + assert (e.code ().category () == generic_category () && + e.code ().value () == ENOSYS); #endif + } rmdir_r (td); } diff --git a/tests/dir-iterator/testscript b/tests/dir-iterator/testscript index b1444ad..720622f 100644 --- a/tests/dir-iterator/testscript +++ b/tests/dir-iterator/testscript @@ -14,42 +14,84 @@ $* a >"reg b" mkdir -p a/b; $* a >"dir b" -# Note that on Windows only directory symlinks are currently supported (see -# mksymlink() for details). -# -: dangling-link +: symlink : -if ($cxx.target.class != 'windows') +: If we are not cross-testing let's test dangling and non-dangling symlynks. +: On Windows that involves mklink command usability test. If we fail to create +: a trial link (say because we are not in the Developer Mode and are running +: non-elevated console), then the test group will be silently skipped. +: +if ($test.target == $build.host) { - +mkdir a - +touch --no-cleanup a/b - +ln -s a/b a/l - +rm a/b + +if ($cxx.target.class != 'windows') + lns = ln -s wd/t wd/l &wd/l + else + echo 'yes' >=t + if cmd /C 'mklink l t' >- 2>- &?l && cat l >'yes' + lns = cmd /C 'mklink wd\l t' &wd/l >- + end - +touch a/c + jnc = cmd /C 'mklink /J wd\l wd\t' &wd/l >- + end - $* ../a >! 2>! != 0 : keep - $* -i ../a >'reg c' : skip -} -else -{ - +mkdir a - +mkdir --no-cleanup a/b - +ln -s a/b a/bl - +rmdir a/b + : symlink + : + if! $empty($lns) + { + : file + : + { + +mkdir wd + +touch --no-cleanup wd/t + +touch wd/f + +$lns + +$* wd >>~%EOO% + %(reg f|reg t|sym reg l)%{3} + EOO + +rm wd/t - +touch a/c + $* ../wd >- 2>! != 0 : keep + $* -i ../wd >'reg f': skip + } - +mkdir a/d - +ln -s a/d a/dl + : dir + : + { + +mkdir wd + +mkdir --no-cleanup wd/t + +mkdir wd/d + +$lns - # On Wine dangling symlinks are not visible (see mksymlink() for details). - # - #$* ../a >! 2>! != 0 : keep + # Note that this test may fail on Windows (see symlinks known issues in + # libbutl/filesystem.mxx). + # + +if ($cxx.target.class != 'windows') + $* wd >>~%EOO% + %(dir d|dir t|sym dir l)%{3} + EOO + end - : skip + +rmdir wd/t + + $* ../wd >- 2>! != 0 : keep + $* -i ../wd >'dir d': skip + } + } + + : junction : - $* -i ../a >>~%EOO% - %(reg c|dir d|sym dir dl)%{3} - EOO + if! $empty($jnc) + { + +mkdir wd + +mkdir --no-cleanup wd/t + +mkdir wd/d + +$jnc + +$* wd >>~%EOO% + %(dir d|dir t|sym dir l)%{3} + EOO + +rmdir wd/t + + $* ../wd >- 2>! != 0 : keep + $* -i ../wd >'dir d': skip + } } diff --git a/tests/fdstream/driver.cxx b/tests/fdstream/driver.cxx index 097383c..3215e02 100644 --- a/tests/fdstream/driver.cxx +++ b/tests/fdstream/driver.cxx @@ -419,26 +419,40 @@ main (int argc, const char* argv[]) ofs.clear (ofdstream::failbit); } -#ifndef _WIN32 - - // Fail for an existing symlink to unexistent file. + // Note that on Windows regular file symlinks may not be supported (see + // mksymlink() for details), so the following tests are allowed to fail + // with ENOSYS on Windows. // - path link (td / path ("link")); - mksymlink (td / path ("unexistent"), link); - try { - fdopen (link, (fdopen_mode::out | - fdopen_mode::create | - fdopen_mode::exclusive)); + // Fail for an existing symlink to unexistent file. + // + path link (td / path ("link")); + mksymlink (td / path ("unexistent"), link); - assert (false); + try + { + fdopen (link, (fdopen_mode::out | + fdopen_mode::create | + fdopen_mode::exclusive)); + + assert (false); + } + catch (const ios::failure&) + { + } } - catch (const ios::failure&) + catch (const system_error& e) { +#ifndef _WIN32 + assert (false); +#else + assert (e.code ().category () == generic_category () && + e.code ().value () == ENOSYS); +#endif } -#else +#ifdef _WIN32 // Check translation modes. // diff --git a/tests/link/driver.cxx b/tests/link/driver.cxx index 96ac880..a77df6f 100644 --- a/tests/link/driver.cxx +++ b/tests/link/driver.cxx @@ -35,18 +35,33 @@ using namespace butl; static const char text[] = "ABCDEF"; +enum class mklink +{ + sym, + hard, + any +}; + static bool -link_file (const path& target, const path& link, bool hard, bool check_content) +link_file (const path& target, const path& link, mklink ml, bool check_content) { try { - if (hard) - mkhardlink (target, link); - else - mksymlink (target, link); + switch (ml) + { + case mklink::sym: mksymlink (target, link); break; + case mklink::hard: mkhardlink (target, link); break; + case mklink::any: mkanylink (target, link, true /* copy */); break; + } } catch (const system_error&) { + //cerr << e << endl; + return false; + } + catch (const pair&) + { + //cerr << e.second << endl; return false; } @@ -139,22 +154,26 @@ main () // Create the file hard link. // - assert (link_file (fp, td / path ("hlink"), true, true)); + assert (link_file (fp, td / path ("hlink"), mklink::hard, true)); #ifndef _WIN32 // Create the file symlink using an absolute path. // - assert (link_file (fp, td / path ("slink"), false, true)); + assert (link_file (fp, td / path ("slink"), mklink::sym, true)); // Create the file symlink using a relative path. // - assert (link_file (fn, td / path ("rslink"), false, true)); + assert (link_file (fn, td / path ("rslink"), mklink::sym, true)); // Create the file symlink using an unexistent file path. // - assert (link_file (fp + "-a", td / path ("sa"), false, false)); + assert (link_file (fp + "-a", td / path ("sa"), mklink::sym, false)); #endif + // Create the file any link. + // + assert (link_file (fp, td / path ("alink"), mklink::any, true)); + // Prepare the target directory. // dir_path dn ("dir"); @@ -169,8 +188,8 @@ main () } #ifndef _WIN32 - assert (link_file (fp, dp / path ("hlink"), true, true)); - assert (link_file (fp, dp / path ("slink"), false, true)); + assert (link_file (fp, dp / path ("hlink"), mklink::hard, true)); + assert (link_file (fp, dp / path ("slink"), mklink::sym, true)); #endif // Create the directory symlink using an absolute path. @@ -250,24 +269,64 @@ main () assert (link_dir (dp, ld, false /* hard */, true /* check_content */)); rmdir_r (dp); - // On Wine dangling junctions are not visible. That's why we also re-create - // the target before the junction removal. - // -#if 0 { pair pe (path_entry (ld)); assert (pe.first && pe.second.type == entry_type::symlink); } -#endif { pair pe (path_entry (ld, true /* follow_symlinks */)); assert (!pe.first); } - assert (try_mkdir (dp) == mkdir_status::success); assert (try_rmsymlink (ld) == rmfile_status::success); + // Try to create a dangling regular file symlink and make sure it is + // properly removed via its parent recursive removal. + // + assert (try_mkdir (dp) == mkdir_status::success); + + // Note that on Windows regular file symlinks may not be supported (see + // mksymlink() for details), so the following tests are allowed to fail + // with ENOSYS on Windows. + // + try + { + mksymlink (dp / "non-existing", dp / "lnk"); + assert (!dir_empty (dp)); + assert (dir_iterator (dp, true /* ignore_dangling */) == dir_iterator ()); + } + catch (const system_error& e) + { +#ifndef _WIN32 + assert (false); +#else + assert (e.code ().category () == generic_category () && + e.code ().value () == ENOSYS); +#endif + } + + rmdir_r (dp); + + // Create a dangling directory symlink and make sure it is properly removed + // via its parent recursive removal. Also make sure that removing directory + // symlink keeps its target intact. + // + assert (try_mkdir (dp) == mkdir_status::success); + + dir_path tgd (td / dir_path ("tdir")); + assert (try_mkdir (tgd) == mkdir_status::success); + + mksymlink (dp / "non-existing", dp / "lnk1", true /* dir */); + assert (!dir_empty (dp)); + assert (dir_iterator (dp, true /* ignore_dangling */) == dir_iterator ()); + + mksymlink (tgd, dp / "lnk2", true /* dir */); + assert (dir_iterator (dp, true /* ignore_dangling */) != dir_iterator ()); + + rmdir_r (dp); + assert (dir_exists (tgd)); + try { rmdir_r (td); diff --git a/tests/mventry/testscript b/tests/mventry/testscript index ecd617a..c6cbf45 100644 --- a/tests/mventry/testscript +++ b/tests/mventry/testscript @@ -92,8 +92,8 @@ : : If we are not cross-testing let's test renaming symlynks from and over. On : Windows that involves mklink command usability test. If we fail to create a -: trial link (say because we are running non-administrative console), then the -: test group will be silently skipped. +: trial link (say because we are not in the Developer Mode and are running +: non-elevated console), then the test group will be silently skipped. : if ($test.target == $build.host) { @@ -104,8 +104,12 @@ if ($test.target == $build.host) if cmd /C 'mklink b a' >- 2>- &?b && cat b >'yes' lns = cmd /C 'mklink b a' >- end + + jnc = cmd /C 'mklink /J b a' >- end + : symlink + : if! $empty($lns) { : file @@ -126,11 +130,11 @@ if ($test.target == $build.host) : to : - : Make sure that if destination is a symlink it is get overwritten and it's - : target stays intact. + : Make sure that if destination is a symlink it is get overwritten and + : it's target stays intact. : echo 'foo' >=a; - $lns; + $lns &b; echo 'bar' >=c &!c; $* c b; cat a >'foo'; @@ -182,6 +186,43 @@ if ($test.target == $build.host) $* b c 2>- == 1 } } + + : junction + : + if! $empty($jnc) + { + : from + : + : Make sure that if source is a junction it refers the same target after + : rename. + : + mkdir -p a; + $jnc &!b; + $* b c &c; + touch a/b; + test -f c/b; + test -d b == 1 + + : to + : + : Make sure that if destination is a junction it is get overwritten and + : it's target stays intact. + : + mkdir -p a; + $jnc; + echo 'foo' >=c &!c; + $* c b &b; + cat b >'foo'; + test -d a; + test -f c == 1 + + : over-existing-dir + : + mkdir a; + $jnc &b; + mkdir c; + $* b c 2>- == 1 + } } : different-fs diff --git a/tests/path-entry/driver.cxx b/tests/path-entry/driver.cxx index d48bf49..51ac04d 100644 --- a/tests/path-entry/driver.cxx +++ b/tests/path-entry/driver.cxx @@ -4,7 +4,9 @@ #include #ifndef __cpp_lib_modules_ts +#include #include +#include // invalid_argument #include #endif @@ -15,48 +17,210 @@ import std.core; import std.io; #endif +import butl.path; import butl.utility; // operator<<(ostream, exception) +import butl.optional; +import butl.timestamp; import butl.filesystem; #else +#include #include +#include +#include #include #endif using namespace std; using namespace butl; -// Usage: argv[0] +// Usage: argv[0] [-l] [-t] [-p ] [-m