diff --git a/cpp/cmake/tests.cmake b/cpp/cmake/tests.cmake index 99a23c6d..2baff82c 100644 --- a/cpp/cmake/tests.cmake +++ b/cpp/cmake/tests.cmake @@ -66,6 +66,9 @@ set(LIBLOOT_INTERFACE_TESTS_ALL_SOURCES "${PROJECT_SOURCE_DIR}/src/tests/test_helpers.h" "${PROJECT_SOURCE_DIR}/src/tests/printers.h") +if(MSVC) + list(APPEND LIBLOOT_INTERFACE_TESTS_ALL_SOURCES "${PROJECT_SOURCE_DIR}/src/tests/libloot_tests.manifest") +endif() ############################## # Define Targets diff --git a/cpp/src/tests/api/interface/main.cpp b/cpp/src/tests/api/interface/main.cpp index 9444f1eb..1df259b0 100644 --- a/cpp/src/tests/api/interface/main.cpp +++ b/cpp/src/tests/api/interface/main.cpp @@ -113,6 +113,12 @@ TEST(Filesystem, u8pathConvertsCharacterEncodingFromUtf8ToNative) { EXPECT_EQ(utf16, path.u16string()); } +#ifdef _WIN32 +TEST(WindowsRegistry, hasLongPathsEnabled) { + EXPECT_TRUE(loot::test::windowsHasLongPathsEnabled()); +} +#endif + namespace loot { namespace test { void testLoggingCallback(LogLevel, std::string_view) { diff --git a/cpp/src/tests/libloot_tests.manifest b/cpp/src/tests/libloot_tests.manifest new file mode 100644 index 00000000..9e2a2728 --- /dev/null +++ b/cpp/src/tests/libloot_tests.manifest @@ -0,0 +1,8 @@ + + + + + true + + + diff --git a/cpp/src/tests/test_helpers.h b/cpp/src/tests/test_helpers.h index 9b8d5a68..f8cec155 100644 --- a/cpp/src/tests/test_helpers.h +++ b/cpp/src/tests/test_helpers.h @@ -31,6 +31,10 @@ along with LOOT. If not, see #include "loot/enum/game_type.h" +#ifdef _WIN32 +#include +#endif + namespace loot::test { bool supportsLightPlugins(GameType gameType) { return gameType == GameType::tes5se || gameType == GameType::tes5vr || @@ -63,6 +67,22 @@ std::filesystem::path getSourceArchivesPath(GameType gameType) { } } +#ifdef _WIN32 +bool windowsHasLongPathsEnabled() { + DWORD value = 0; + DWORD valueSize = sizeof(value); + LONG ret = RegGetValue(HKEY_LOCAL_MACHINE, + L"SYSTEM\\CurrentControlSet\\Control\\FileSystem", + L"LongPathsEnabled", + RRF_RT_REG_DWORD, + NULL, + &value, + &valueSize); + + return ret == ERROR_SUCCESS && value == 1; +} +#endif + std::filesystem::path getRootTestPath() { std::random_device randomDevice; std::default_random_engine prng(randomDevice()); @@ -77,8 +97,16 @@ std::filesystem::path getRootTestPath() { directoryName.push_back(static_cast(dist(prng))); } - return std::filesystem::absolute(std::filesystem::temp_directory_path() / - std::filesystem::u8path(directoryName)); + auto tempPath = std::filesystem::temp_directory_path() / + std::filesystem::u8path(directoryName); + +#ifdef _WIN32 + if (windowsHasLongPathsEnabled()) { + tempPath /= std::filesystem::u8path(std::string(255, 'a')); + } +#endif + + return std::filesystem::absolute(tempPath); } } diff --git a/src/game.rs b/src/game.rs index 4b2ad4b1..aac48daf 100644 --- a/src/game.rs +++ b/src/game.rs @@ -834,6 +834,8 @@ mod tests { use super::*; + const MAX_PATH_COMPONENT: usize = 255; + #[cfg(windows)] fn symlink_dir(original: &Path, link: &Path) { std::os::windows::fs::symlink_dir(original, link).unwrap(); @@ -957,13 +959,14 @@ mod tests { #[test] fn should_succeed_if_given_a_symlink_path() { - let fixture = Fixture::new(GameType::Morrowind); + let fixture = Fixture::without_long_paths(GameType::Morrowind); - let game_path = fixture.game_path.with_extension("symlink"); - symlink_dir(&fixture.game_path, &game_path); - assert!(game_path.is_symlink()); + let symlink_game_path = fixture.root_path().join("a".repeat(MAX_PATH_COMPONENT)); - assert!(Game::new(fixture.game_type, &game_path).is_ok()); + symlink_dir(&fixture.game_path, &symlink_game_path); + assert!(symlink_game_path.is_symlink()); + + assert!(Game::new(fixture.game_type, &symlink_game_path).is_ok()); } #[cfg(windows)] @@ -1036,17 +1039,21 @@ mod tests { #[test] fn should_succeed_if_given_symlink_paths() { - let fixture = Fixture::new(GameType::Oblivion); + let fixture = Fixture::without_long_paths(GameType::Oblivion); - let game_path = fixture.game_path.with_extension("symlink"); - symlink_dir(&fixture.game_path, &game_path); - assert!(game_path.is_symlink()); + let symlink_game_path = fixture.root_path().join("a".repeat(MAX_PATH_COMPONENT)); + symlink_dir(&fixture.game_path, &symlink_game_path); + assert!(symlink_game_path.is_symlink()); - let local_path = fixture.local_path.with_extension("symlink"); - symlink_dir(&fixture.local_path, &local_path); - assert!(local_path.is_symlink()); + let symlink_local_path = fixture.root_path().join("b".repeat(MAX_PATH_COMPONENT)); + symlink_dir(&fixture.local_path, &symlink_local_path); + assert!(symlink_local_path.is_symlink()); - let game = Game::with_local_path(fixture.game_type, &game_path, &local_path); + let game = Game::with_local_path( + fixture.game_type, + &symlink_game_path, + &symlink_local_path, + ); assert!(game.is_ok()); } diff --git a/src/tests.rs b/src/tests.rs index abf607a4..b8f241c5 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -177,35 +177,119 @@ fn data_path(game_type: GameType, game_path: &Path) -> PathBuf { } pub(crate) struct Fixture { - _temp_dir: TempDir, + temp_dir: TempDir, pub(crate) game_type: GameType, pub(crate) game_path: PathBuf, pub(crate) local_path: PathBuf, } +fn copy_starfield_plugins(source_plugins_path: &Path, data_path: &Path) { + copy_file(source_plugins_path, data_path, BLANK_FULL_ESM); + copy_file(source_plugins_path, data_path, BLANK_MEDIUM_ESM); + + copy( + source_plugins_path.join(BLANK_FULL_ESM), + data_path.join(BLANK_ESM), + ) + .unwrap(); + copy( + source_plugins_path.join(BLANK_FULL_ESM), + data_path.join(BLANK_DIFFERENT_ESM), + ) + .unwrap(); + copy( + source_plugins_path.join("Blank - Override.full.esm"), + data_path.join(BLANK_MASTER_DEPENDENT_ESM), + ) + .unwrap(); + + copy_file(source_plugins_path, data_path, BLANK_ESP); + copy( + source_plugins_path.join(BLANK_ESP), + data_path.join(BLANK_DIFFERENT_ESP), + ) + .unwrap(); + copy( + source_plugins_path.join(BLANK_OVERRIDE_ESP), + data_path.join(BLANK_MASTER_DEPENDENT_ESP), + ) + .unwrap(); +} + +fn copy_plugins(source_plugins_path: &Path, data_path: &Path) { + copy_file(source_plugins_path, data_path, BLANK_ESM); + copy_file(source_plugins_path, data_path, BLANK_DIFFERENT_ESM); + copy_file(source_plugins_path, data_path, BLANK_MASTER_DEPENDENT_ESM); + copy_file( + source_plugins_path, + data_path, + BLANK_DIFFERENT_MASTER_DEPENDENT_ESM, + ); + + copy_file(source_plugins_path, data_path, BLANK_ESP); + copy_file(source_plugins_path, data_path, BLANK_DIFFERENT_ESP); + copy_file(source_plugins_path, data_path, BLANK_MASTER_DEPENDENT_ESP); + copy_file( + source_plugins_path, + data_path, + BLANK_DIFFERENT_MASTER_DEPENDENT_ESP, + ); + copy_file(source_plugins_path, data_path, BLANK_PLUGIN_DEPENDENT_ESP); + copy_file( + source_plugins_path, + data_path, + BLANK_DIFFERENT_PLUGIN_DEPENDENT_ESP, + ); +} + impl Fixture { + const PREFIX: &str = "libloot-t\u{00E9}st-"; + pub(crate) fn new(game_type: GameType) -> Self { let temp_dir = tempfile::Builder::new() - .prefix("libloot-t\u{00E9}st-") + .prefix(Self::PREFIX) .tempdir() .unwrap(); - Self::with_tempdir(game_type, temp_dir) + Self::with_tempdir(game_type, temp_dir, true) } - pub(crate) fn in_path(game_type: GameType, in_path: &Path) -> Fixture { + /// This creates the fixture without intentionally creating paths that are + /// more than 260 characters long. It's intended for use when testing + /// symlinks, because Windows will only allow a test to create a symlink to + /// a long path if Windows is configured with long paths enabled and the + /// test executable also has a manifest that sets longPathAware to true, but + /// embedding a manifest isn't straightforward in Rust without the use of + /// build dependencies, which would also be built for non-test code. + /// The same functionality is more easily tested in the C++ wrapper, so it's + /// done there instead. + pub(crate) fn without_long_paths(game_type: GameType) -> Self { let temp_dir = tempfile::Builder::new() - .prefix("libloot-t\u{00E9}st-") + .prefix(Self::PREFIX) + .tempdir() + .unwrap(); + + Self::with_tempdir(game_type, temp_dir, false) + } + + pub(crate) fn in_path(game_type: GameType, in_path: &Path) -> Self { + let temp_dir = tempfile::Builder::new() + .prefix(Self::PREFIX) .tempdir_in(in_path) .unwrap(); - Self::with_tempdir(game_type, temp_dir) + Self::with_tempdir(game_type, temp_dir, true) } - fn with_tempdir(game_type: GameType, temp_dir: TempDir) -> Fixture { - let root_path = temp_dir.path(); - let game_path = root_path.join("games/game"); - let local_path = root_path.join("local/game"); + fn with_tempdir(game_type: GameType, temp_dir: TempDir, use_long_path: bool) -> Self { + let base_path = if use_long_path { + temp_dir.path().join("a".repeat(255)) + } else { + temp_dir.path().to_path_buf() + }; + + let game_path = base_path.join("games").join("game"); + let local_path = base_path.join("local").join("game"); let data_path = data_path(game_type, &game_path); create_dir_all(&data_path).unwrap(); @@ -214,58 +298,9 @@ impl Fixture { let source_plugins_path = source_plugins_path(game_type); if game_type == GameType::Starfield { - copy_file(&source_plugins_path, &data_path, BLANK_FULL_ESM); - copy_file(&source_plugins_path, &data_path, BLANK_MEDIUM_ESM); - - copy( - source_plugins_path.join(BLANK_FULL_ESM), - data_path.join(BLANK_ESM), - ) - .unwrap(); - copy( - source_plugins_path.join(BLANK_FULL_ESM), - data_path.join(BLANK_DIFFERENT_ESM), - ) - .unwrap(); - copy( - source_plugins_path.join("Blank - Override.full.esm"), - data_path.join(BLANK_MASTER_DEPENDENT_ESM), - ) - .unwrap(); - copy_file(&source_plugins_path, &data_path, BLANK_ESP); - copy( - source_plugins_path.join(BLANK_ESP), - data_path.join(BLANK_DIFFERENT_ESP), - ) - .unwrap(); - copy( - source_plugins_path.join(BLANK_OVERRIDE_ESP), - data_path.join(BLANK_MASTER_DEPENDENT_ESP), - ) - .unwrap(); + copy_starfield_plugins(&source_plugins_path, &data_path); } else { - copy_file(&source_plugins_path, &data_path, BLANK_ESM); - copy_file(&source_plugins_path, &data_path, BLANK_DIFFERENT_ESM); - copy_file(&source_plugins_path, &data_path, BLANK_MASTER_DEPENDENT_ESM); - copy_file( - &source_plugins_path, - &data_path, - BLANK_DIFFERENT_MASTER_DEPENDENT_ESM, - ); - copy_file(&source_plugins_path, &data_path, BLANK_ESP); - copy_file(&source_plugins_path, &data_path, BLANK_DIFFERENT_ESP); - copy_file(&source_plugins_path, &data_path, BLANK_MASTER_DEPENDENT_ESP); - copy_file( - &source_plugins_path, - &data_path, - BLANK_DIFFERENT_MASTER_DEPENDENT_ESP, - ); - copy_file(&source_plugins_path, &data_path, BLANK_PLUGIN_DEPENDENT_ESP); - copy_file( - &source_plugins_path, - &data_path, - BLANK_DIFFERENT_PLUGIN_DEPENDENT_ESP, - ); + copy_plugins(&source_plugins_path, &data_path); } if supports_light_plugins(game_type) { @@ -307,7 +342,7 @@ impl Fixture { .unwrap(); Self { - _temp_dir: temp_dir, + temp_dir, game_type, game_path, local_path, @@ -317,6 +352,10 @@ impl Fixture { pub(crate) fn data_path(&self) -> PathBuf { data_path(self.game_type, &self.game_path) } + + pub(crate) fn root_path(&self) -> &Path { + self.temp_dir.path() + } } #[test_parameter]