diff --git a/.travis.yml b/.travis.yml index 5e1487e..f3597fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -56,5 +56,5 @@ before_script: script: - export VERBOSE=1 - cmake --build . --config Release - - ctest + - ctest -C Release -E Windows diff --git a/include/ghc/filesystem.hpp b/include/ghc/filesystem.hpp index 70ce584..e2a9ca5 100644 --- a/include/ghc/filesystem.hpp +++ b/include/ghc/filesystem.hpp @@ -75,7 +75,7 @@ #elif defined(GHC_FILESYSTEM_FWD) #define GHC_INLINE #ifdef GHC_OS_WINDOWS -#define GHC_FS_API extern +#define GHC_FS_API extern #define GHC_FS_API_CLASS #else #define GHC_FS_API extern @@ -1259,8 +1259,7 @@ namespace detail { GHC_INLINE bool startsWith(const std::string& what, const std::string& with) { - return with.length() <= what.length() - && equal(with.begin(), with.end(), what.begin()); + return with.length() <= what.length() && equal(with.begin(), with.end(), what.begin()); } GHC_INLINE void postprocess_path_with_format(path::string_type& p, path::format fmt) @@ -1277,11 +1276,11 @@ GHC_INLINE void postprocess_path_with_format(path::string_type& p, path::format case path::auto_format: case path::native_format: #endif - if(startsWith(p, std::string("\\\\?\\"))) { + if (startsWith(p, std::string("\\\\?\\"))) { // remove Windows long filename marker p.erase(0, 4); - if(startsWith(p, std::string("UNC\\"))) { - p.erase(0,2); + if (startsWith(p, std::string("UNC\\"))) { + p.erase(0, 2); p[0] = '\\'; } } @@ -1356,7 +1355,15 @@ namespace detail { GHC_INLINE bool compare_no_case(const char* str1, const char* str2) { #ifdef GHC_OS_WINDOWS +# ifdef __GNUC__ + while (::tolower((unsigned char)*str1) == ::tolower((unsigned char)*str2++)) { + if (*str1++ == 0) + return false; + } + return true; +# else return ::_stricmp(str1, str2); +# endif #else return ::strcasecmp(str1, str2); #endif @@ -1530,10 +1537,8 @@ GHC_INLINE path resolveSymlink(const path& p, std::error_code& ec) char buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE] = {0}; REPARSE_DATA_BUFFER& reparseData = *(REPARSE_DATA_BUFFER*)buffer; ULONG bufferUsed; - ULONG dwError; path result; if (DeviceIoControl(file.get(), FSCTL_GET_REPARSE_POINT, 0, 0, &reparseData, sizeof(buffer), &bufferUsed, 0)) { - dwError = NOERROR; if (IsReparseTagMicrosoft(reparseData.ReparseTag)) { switch (reparseData.ReparseTag) { case IO_REPARSE_TAG_SYMLINK: @@ -2080,9 +2085,9 @@ GHC_INLINE void path::swap(path& rhs) noexcept GHC_INLINE const path::string_type& path::native() const { #ifdef GHC_OS_WINDOWS - if(is_absolute() && _path.length() >= MAX_PATH) { + if (is_absolute() && _path.length() > MAX_PATH - 10) { // expand long Windows filenames with marker - if(has_root_name() && _path[0] == '/') { + if (has_root_name() && _path[0] == '/') { _native_cache = "\\\\?\\UNC" + _path.substr(1); } else { @@ -2850,7 +2855,10 @@ GHC_INLINE path canonical(const path& p, std::error_code& ec) result = result.parent_path(); continue; } - + else if ((result/pe).string().length() <= root.string().length()) { + result /= pe; + continue; + } auto sls = symlink_status(result / pe, ec); if (ec) { return path(); @@ -3700,6 +3708,16 @@ GHC_INLINE void permissions(const path& p, perms prms, perm_options opts, std::e } } #ifdef GHC_OS_WINDOWS +# ifdef __GNUC__ + auto oldAttr = GetFileAttributesW(p.wstring().c_str()); + if (oldAttr != INVALID_FILE_ATTRIBUTES) { + DWORD newAttr = ((prms & perms::owner_write) == perms::owner_write) ? oldAttr & ~FILE_ATTRIBUTE_READONLY : oldAttr | FILE_ATTRIBUTE_READONLY; + if (oldAttr == newAttr || SetFileAttributesW(p.wstring().c_str(), newAttr)) { + return; + } + } + ec = std::error_code(::GetLastError(), std::system_category()); +# else int mode = 0; if ((prms & perms::owner_read) == perms::owner_read) { mode |= _S_IREAD; @@ -3710,6 +3728,7 @@ GHC_INLINE void permissions(const path& p, perms prms, perm_options opts, std::e if (::_wchmod(p.wstring().c_str(), mode) != 0) { ec = std::error_code(::GetLastError(), std::system_category()); } +# endif #else if ((opts & perm_options::nofollow) != perm_options::nofollow) { if (::chmod(p.c_str(), static_cast(prms)) != 0) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e8a2f13..96268dd 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,4 +1,8 @@ +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake/") +set(PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS ON) +include(ParseAndAddCatchTests) + add_executable(filesystem_test filesystem_test.cpp catch.hpp) target_link_libraries(filesystem_test ghc_filesystem) target_compile_options(filesystem_test PRIVATE @@ -13,7 +17,7 @@ if(CMAKE_GENERATOR STREQUAL Xcode) target_compile_options(filesystem_test_cov PRIVATE "$<$:--coverage>") target_link_libraries(filesystem_test_cov PUBLIC ghc_filesystem PRIVATE --coverage) endif() -add_test(filesystem_test filesystem_test) +ParseAndAddCatchTests(filesystem_test filesystem_test) if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang" AND (CMAKE_CXX_COMPILER_VERSION VERSION_EQUAL 7.0 OR CMAKE_CXX_COMPILER_VERSION VERSION_GREATER 7.0)) if(APPLE) diff --git a/test/cmake/ParseAndAddCatchTests.cmake b/test/cmake/ParseAndAddCatchTests.cmake new file mode 100644 index 0000000..925d932 --- /dev/null +++ b/test/cmake/ParseAndAddCatchTests.cmake @@ -0,0 +1,225 @@ +#==================================================================================================# +# supported macros # +# - TEST_CASE, # +# - SCENARIO, # +# - TEST_CASE_METHOD, # +# - CATCH_TEST_CASE, # +# - CATCH_SCENARIO, # +# - CATCH_TEST_CASE_METHOD. # +# # +# Usage # +# 1. make sure this module is in the path or add this otherwise: # +# set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake.modules/") # +# 2. make sure that you've enabled testing option for the project by the call: # +# enable_testing() # +# 3. add the lines to the script for testing target (sample CMakeLists.txt): # +# project(testing_target) # +# set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake.modules/") # +# enable_testing() # +# # +# find_path(CATCH_INCLUDE_DIR "catch.hpp") # +# include_directories(${INCLUDE_DIRECTORIES} ${CATCH_INCLUDE_DIR}) # +# # +# file(GLOB SOURCE_FILES "*.cpp") # +# add_executable(${PROJECT_NAME} ${SOURCE_FILES}) # +# # +# include(ParseAndAddCatchTests) # +# ParseAndAddCatchTests(${PROJECT_NAME}) # +# # +# The following variables affect the behavior of the script: # +# # +# PARSE_CATCH_TESTS_VERBOSE (Default OFF) # +# -- enables debug messages # +# PARSE_CATCH_TESTS_NO_HIDDEN_TESTS (Default OFF) # +# -- excludes tests marked with [!hide], [.] or [.foo] tags # +# PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME (Default ON) # +# -- adds fixture class name to the test name # +# PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME (Default ON) # +# -- adds cmake target name to the test name # +# PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS (Default OFF) # +# -- causes CMake to rerun when file with tests changes so that new tests will be discovered # +# # +# One can also set (locally) the optional variable OptionalCatchTestLauncher to precise the way # +# a test should be run. For instance to use test MPI, one can write # +# set(OptionalCatchTestLauncher ${MPIEXEC} ${MPIEXEC_NUMPROC_FLAG} ${NUMPROC}) # +# just before calling this ParseAndAddCatchTests function # +# # +# The AdditionalCatchParameters optional variable can be used to pass extra argument to the test # +# command. For example, to include successful tests in the output, one can write # +# set(AdditionalCatchParameters --success) # +# # +# After the script, the ParseAndAddCatchTests_TESTS property for the target, and for each source # +# file in the target is set, and contains the list of the tests extracted from that target, or # +# from that file. This is useful, for example to add further labels or properties to the tests. # +# # +#==================================================================================================# + +if (CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 2.8.8) + message(FATAL_ERROR "ParseAndAddCatchTests requires CMake 2.8.8 or newer") +endif() + +option(PARSE_CATCH_TESTS_VERBOSE "Print Catch to CTest parser debug messages" OFF) +option(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS "Exclude tests with [!hide], [.] or [.foo] tags" OFF) +option(PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME "Add fixture class name to the test name" ON) +option(PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME "Add target name to the test name" ON) +option(PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS "Add test file to CMAKE_CONFIGURE_DEPENDS property" OFF) + +function(ParseAndAddCatchTests_PrintDebugMessage) + if(PARSE_CATCH_TESTS_VERBOSE) + message(STATUS "ParseAndAddCatchTests: ${ARGV}") + endif() +endfunction() + +# This removes the contents between +# - block comments (i.e. /* ... */) +# - full line comments (i.e. // ... ) +# contents have been read into '${CppCode}'. +# !keep partial line comments +function(ParseAndAddCatchTests_RemoveComments CppCode) + string(ASCII 2 CMakeBeginBlockComment) + string(ASCII 3 CMakeEndBlockComment) + string(REGEX REPLACE "/\\*" "${CMakeBeginBlockComment}" ${CppCode} "${${CppCode}}") + string(REGEX REPLACE "\\*/" "${CMakeEndBlockComment}" ${CppCode} "${${CppCode}}") + string(REGEX REPLACE "${CMakeBeginBlockComment}[^${CMakeEndBlockComment}]*${CMakeEndBlockComment}" "" ${CppCode} "${${CppCode}}") + string(REGEX REPLACE "\n[ \t]*//+[^\n]+" "\n" ${CppCode} "${${CppCode}}") + + set(${CppCode} "${${CppCode}}" PARENT_SCOPE) +endfunction() + +# Worker function +function(ParseAndAddCatchTests_ParseFile SourceFile TestTarget) + # If SourceFile is an object library, do not scan it (as it is not a file). Exit without giving a warning about a missing file. + if(SourceFile MATCHES "\\\$") + ParseAndAddCatchTests_PrintDebugMessage("Detected OBJECT library: ${SourceFile} this will not be scanned for tests.") + return() + endif() + # According to CMake docs EXISTS behavior is well-defined only for full paths. + get_filename_component(SourceFile ${SourceFile} ABSOLUTE) + if(NOT EXISTS ${SourceFile}) + message(WARNING "Cannot find source file: ${SourceFile}") + return() + endif() + ParseAndAddCatchTests_PrintDebugMessage("parsing ${SourceFile}") + file(STRINGS ${SourceFile} Contents NEWLINE_CONSUME) + + # Remove block and fullline comments + ParseAndAddCatchTests_RemoveComments(Contents) + + # Find definition of test names + string(REGEX MATCHALL "[ \t]*(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)[ \t]*\\([^\)]+\\)+[ \t\n]*{+[ \t]*(//[^\n]*[Tt][Ii][Mm][Ee][Oo][Uu][Tt][ \t]*[0-9]+)*" Tests "${Contents}") + + if(PARSE_CATCH_TESTS_ADD_TO_CONFIGURE_DEPENDS AND Tests) + ParseAndAddCatchTests_PrintDebugMessage("Adding ${SourceFile} to CMAKE_CONFIGURE_DEPENDS property") + set_property( + DIRECTORY + APPEND + PROPERTY CMAKE_CONFIGURE_DEPENDS ${SourceFile} + ) + endif() + + foreach(TestName ${Tests}) + # Strip newlines + string(REGEX REPLACE "\\\\\n|\n" "" TestName "${TestName}") + + # Get test type and fixture if applicable + string(REGEX MATCH "(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)[ \t]*\\([^,^\"]*" TestTypeAndFixture "${TestName}") + string(REGEX MATCH "(CATCH_)?(TEST_CASE_METHOD|SCENARIO|TEST_CASE)" TestType "${TestTypeAndFixture}") + string(REGEX REPLACE "${TestType}\\([ \t]*" "" TestFixture "${TestTypeAndFixture}") + + # Get string parts of test definition + string(REGEX MATCHALL "\"+([^\\^\"]|\\\\\")+\"+" TestStrings "${TestName}") + + # Strip wrapping quotation marks + string(REGEX REPLACE "^\"(.*)\"$" "\\1" TestStrings "${TestStrings}") + string(REPLACE "\";\"" ";" TestStrings "${TestStrings}") + + # Validate that a test name and tags have been provided + list(LENGTH TestStrings TestStringsLength) + if(TestStringsLength GREATER 2 OR TestStringsLength LESS 1) + message(FATAL_ERROR "You must provide a valid test name and tags for all tests in ${SourceFile}") + endif() + + # Assign name and tags + list(GET TestStrings 0 Name) + if("${TestType}" STREQUAL "SCENARIO") + set(Name "Scenario: ${Name}") + endif() + if(PARSE_CATCH_TESTS_ADD_FIXTURE_IN_TEST_NAME AND TestFixture) + set(CTestName "${TestFixture}:${Name}") + else() + set(CTestName "${Name}") + endif() + if(PARSE_CATCH_TESTS_ADD_TARGET_IN_TEST_NAME) + set(CTestName "${TestTarget}:${CTestName}") + endif() + # add target to labels to enable running all tests added from this target + set(Labels ${TestTarget}) + if(TestStringsLength EQUAL 2) + list(GET TestStrings 1 Tags) + string(TOLOWER "${Tags}" Tags) + # remove target from labels if the test is hidden + if("${Tags}" MATCHES ".*\\[!?(hide|\\.)\\].*") + list(REMOVE_ITEM Labels ${TestTarget}) + endif() + string(REPLACE "]" ";" Tags "${Tags}") + string(REPLACE "[" "" Tags "${Tags}") + else() + # unset tags variable from previous loop + unset(Tags) + endif() + + list(APPEND Labels ${Tags}) + + set(HiddenTagFound OFF) + foreach(label ${Labels}) + string(REGEX MATCH "^!hide|^\\." result ${label}) + if(result) + set(HiddenTagFound ON) + break() + endif(result) + endforeach(label) + if(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS AND ${HiddenTagFound} AND ${CMAKE_VERSION} VERSION_LESS "3.9") + ParseAndAddCatchTests_PrintDebugMessage("Skipping test \"${CTestName}\" as it has [!hide], [.] or [.foo] label") + else() + ParseAndAddCatchTests_PrintDebugMessage("Adding test \"${CTestName}\"") + if(Labels) + ParseAndAddCatchTests_PrintDebugMessage("Setting labels to ${Labels}") + endif() + + # Escape commas in the test spec + string(REPLACE "," "\\," Name ${Name}) + + # Add the test and set its properties + add_test(NAME "\"${CTestName}\"" COMMAND ${OptionalCatchTestLauncher} $ ${Name} ${AdditionalCatchParameters}) + # Old CMake versions do not document VERSION_GREATER_EQUAL, so we use VERSION_GREATER with 3.8 instead + if(PARSE_CATCH_TESTS_NO_HIDDEN_TESTS AND ${HiddenTagFound} AND ${CMAKE_VERSION} VERSION_GREATER "3.8") + ParseAndAddCatchTests_PrintDebugMessage("Setting DISABLED test property") + set_tests_properties("\"${CTestName}\"" PROPERTIES DISABLED ON) + else() + set_tests_properties("\"${CTestName}\"" PROPERTIES FAIL_REGULAR_EXPRESSION "No tests ran" + LABELS "${Labels}") + endif() + set_property( + TARGET ${TestTarget} + APPEND + PROPERTY ParseAndAddCatchTests_TESTS "\"${CTestName}\"") + set_property( + SOURCE ${SourceFile} + APPEND + PROPERTY ParseAndAddCatchTests_TESTS "\"${CTestName}\"") + endif() + + + endforeach() +endfunction() + +# entry point +function(ParseAndAddCatchTests TestTarget) + ParseAndAddCatchTests_PrintDebugMessage("Started parsing ${TestTarget}") + get_target_property(SourceFiles ${TestTarget} SOURCES) + ParseAndAddCatchTests_PrintDebugMessage("Found the following sources: ${SourceFiles}") + foreach(SourceFile ${SourceFiles}) + ParseAndAddCatchTests_ParseFile(${SourceFile} ${TestTarget}) + endforeach() + ParseAndAddCatchTests_PrintDebugMessage("Finished parsing ${TestTarget}") +endfunction() diff --git a/test/filesystem_test.cpp b/test/filesystem_test.cpp index 6e81937..55bfb81 100644 --- a/test/filesystem_test.cpp +++ b/test/filesystem_test.cpp @@ -39,8 +39,10 @@ #include #include #include -#ifdef WIN32 -#define NOMINMAX +#if defined(WIN32) || defined(_WIN32) +#ifndef __GNUC__ +#define NOMINMAX 1 +#endif #include #else #include @@ -70,7 +72,6 @@ using fstream = std::fstream; #ifdef GHC_FILESYSTEM_FWD_TEST #include #else -#define NOMINMAX #include #endif namespace fs { @@ -174,32 +175,57 @@ static void generateFile(const fs::path& pathname, int withSize = -1) } #ifdef GHC_OS_WINDOWS +inline bool isWow64Proc() +{ + typedef BOOL(WINAPI * IsWow64Process_t)(HANDLE, PBOOL); + BOOL bIsWow64 = FALSE; + auto fnIsWow64Process = (IsWow64Process_t)GetProcAddress(GetModuleHandle(TEXT("kernel32")), "IsWow64Process"); + if (NULL != fnIsWow64Process) { + if (!fnIsWow64Process(GetCurrentProcess(), &bIsWow64)) { + bIsWow64 = FALSE; + } + } + return bIsWow64; +} + static bool is_symlink_creation_supported() { + bool result = true; HKEY key; REGSAM flags = KEY_READ; #ifdef _WIN64 flags |= KEY_WOW64_64KEY; -#else - BOOL f64 = FALSE; - if (IsWow64Process(GetCurrentProcess(), &f64) && f64) { +#elif defined(KEY_WOW64_64KEY) + if (isWow64Proc()) { flags |= KEY_WOW64_64KEY; } else { flags |= KEY_WOW64_32KEY; } +#else + result = false; #endif - auto err = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock", 0, flags, &key); - if (err != ERROR_SUCCESS) { - return false; + if (result) { + auto err = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock", 0, flags, &key); + if (err == ERROR_SUCCESS) { + DWORD val = 0, size = sizeof(DWORD); + err = RegQueryValueExW(key, L"AllowDevelopmentWithoutDevLicense", 0, NULL, reinterpret_cast(&val), &size); + RegCloseKey(key); + if (err != ERROR_SUCCESS) { + result = false; + } + else { + result = (val != 0); + } + } + else { + result = false; + } } - DWORD val = 0, size = sizeof(DWORD); - err = RegQueryValueExW(key, L"AllowDevelopmentWithoutDevLicense", 0, NULL, reinterpret_cast(&val), &size); - RegCloseKey(key); - if (err != ERROR_SUCCESS) { - return false; + if (!result) { + std::clog << "Warning: Symlink creation not supported." << std::endl; } - return val != 0; + return result; } #else static bool is_symlink_creation_supported() @@ -1058,7 +1084,7 @@ TEST_CASE("30.10.12 class directory_entry", "[filesystem][directory_entry][fs.di CHECK(de.hard_link_count() == 1); } -TEST_CASE("30.10.13 class directory_iterator", "[filesystem][directory_iterator]") +TEST_CASE("30.10.13 class directory_iterator", "[filesystem][directory_iterator][fs.class.directory_iterator]") { { TemporaryDirectory t; @@ -1107,7 +1133,7 @@ TEST_CASE("30.10.13 class directory_iterator", "[filesystem][directory_iterator] } } -TEST_CASE("30.10.14 class recursive_directory_iterator", "[filesystem][recursive_directory_iterator]") +TEST_CASE("30.10.14 class recursive_directory_iterator", "[filesystem][recursive_directory_iterator][fs.class.rec.dir.itr]") { { TemporaryDirectory t; @@ -1246,7 +1272,7 @@ TEST_CASE("30.10.15.3 copy", "[filesystem][operations][fs.op.copy]") CHECK(fs::exists("dir4/file2")); CHECK(fs::exists("dir4/dir2/file3")); } - if(is_symlink_creation_supported()) { + if (is_symlink_creation_supported()) { TemporaryDirectory t(TempOpt::change_path); std::error_code ec; fs::create_directory("dir1"); @@ -1427,7 +1453,7 @@ TEST_CASE("30.10.15.7 create_directory", "[filesystem][operations][fs.op.create_ TEST_CASE("30.10.15.8 create_directory_symlink", "[filesystem][operations][fs.op.create_directory_symlink]") { - if(is_symlink_creation_supported()) { + if (is_symlink_creation_supported()) { TemporaryDirectory t; fs::create_directory(t.path() / "dir1"); generateFile(t.path() / "dir1/test1"); @@ -1463,7 +1489,7 @@ TEST_CASE("30.10.15.9 create_hard_link", "[filesystem][operations][fs.op.create_ TEST_CASE("30.10.15.10 create_symlink", "[filesystem][operations][fs.op.create_symlink]") { - if(is_symlink_creation_supported()) { + if (is_symlink_creation_supported()) { TemporaryDirectory t; fs::create_directory(t.path() / "dir1"); generateFile(t.path() / "dir1/test1"); @@ -1770,7 +1796,7 @@ TEST_CASE_METHOD(FileTypeMixFixture, "30.10.15.18 is_directory", "[filesystem][o CHECK(!fs::is_directory(fs::file_status(fs::file_type::unknown))); } -TEST_CASE("30.10.15.19 is_empty", "[filesystem][options][fs.op.is_empty]") +TEST_CASE("30.10.15.19 is_empty", "[filesystem][operations][fs.op.is_empty]") { TemporaryDirectory t(TempOpt::change_path); std::error_code ec; @@ -2038,7 +2064,7 @@ TEST_CASE("30.10.15.27 proximate", "[filesystem][operations][fs.op.proximate]") TEST_CASE("30.10.15.28 read_symlink", "[filesystem][operations][fs.op.read_symlink]") { - if(is_symlink_creation_supported()) { + if (is_symlink_creation_supported()) { TemporaryDirectory t(TempOpt::change_path); std::error_code ec; generateFile("foo"); @@ -2272,3 +2298,36 @@ TEST_CASE("30.10.15.39 weakly_canonical", "[filesystem][operations][fs.op.weakly CHECK(fs::weakly_canonical(rel / "d1/../f1/../f2") == dir / "f2"); } } + +#ifdef GHC_OS_WINDOWS +TEST_CASE("Windows: Long filename support", "[filesystem][path][fs.path.win.long]") +{ + TemporaryDirectory t(TempOpt::change_path); + char c = 'A'; + fs::path dir = fs::current_path(); + for (; c <= 'Z'; ++c) { + std::string part = std::string(16, c); + dir /= part; + CHECK_NOTHROW(fs::create_directory(dir)); + CHECK(fs::exists(dir)); + generateFile(dir / "f0"); + CHECK(fs::exists(dir / "f0")); + std::string native = dir.u8string(); + if (native.substr(0, 4) == "\\\\?\\") { + break; + } + } + CHECK(c <= 'Z'); +} + +TEST_CASE("Windows: UNC path support", "[filesystem][path][fs.path.win.unc]") +{ + std::error_code ec; + fs::path p(R"(\\localhost\c$\Windows)"); + auto symstat = fs::symlink_status(p, ec); + CHECK(!ec); + auto p2 = fs::canonical(p, ec); + CHECK(!ec); + CHECK(p2 == p); +} +#endif