diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
| commit | 7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch) | |
| tree | 27fb39be241fdb5ac2734c574de678977d1856d0 /libs/usvfs/test | |
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem,
Proton/umu-run integration, and Flatpak packaging.
Key features:
- FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak)
- Proton/GE-Proton/umu-run launcher with env var forwarding
- Flatpak support (sandbox-aware VFS, NXM handler, umu-run)
- Wine prefix management UI
- Case-insensitive path resolution for Linux filesystems
- QSettings-safe INI handling (avoids Bethesda INI corruption)
- Portable instance support with auto-generated launcher scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/usvfs/test')
169 files changed, 6659 insertions, 0 deletions
diff --git a/libs/usvfs/test/CMakeLists.txt b/libs/usvfs/test/CMakeLists.txt new file mode 100644 index 0000000..c34b31f --- /dev/null +++ b/libs/usvfs/test/CMakeLists.txt @@ -0,0 +1,74 @@ +cmake_minimum_required(VERSION 3.16) + +include(CMakeParseArguments) + +#! usvfs_set_test_properties +# +# this function sets the following properties on the given executable or shared +# library test target: +# - OUTPUT_NAME to add arch-specific prefix +# - OUTPUT_DIRECTORY to put the test executable or shared library in the right location +# - FOLDER to organize the VS solution layout +# +# \param:FOLDER if present, specifies the subfolder to use in the solution +# +function(usvfs_set_test_properties TARGET) + cmake_parse_arguments(USVFS_TEST "" "FOLDER" "" ${ARGN}) + if (NOT DEFINED USVFS_TEST_FOLDER) + set(folder "tests") + else() + set(folder "tests/${USVFS_TEST_FOLDER}") + endif() + set_target_properties(${TARGET} + PROPERTIES + FOLDER ${folder} + RUNTIME_OUTPUT_NAME ${TARGET}${ARCH_POSTFIX} + RUNTIME_OUTPUT_DIRECTORY_DEBUG ${USVFS_TEST_BINDIR} + RUNTIME_OUTPUT_DIRECTORY_RELEASE ${USVFS_TEST_BINDIR} + RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO ${USVFS_TEST_BINDIR} + ) +endfunction() + +#! usvfs_target_link_usvfs +# +# add a target link between the given target and the usvfs shared library with delay +# loading +# +function(usvfs_target_link_usvfs TARGET) + target_link_libraries(${TARGET} PRIVATE usvfs_dll) + target_link_options(${TARGET} PRIVATE "/DELAYLOAD:usvfs${ARCH_POSTFIX}.dll") +endfunction() + + +file(GLOB directories LIST_DIRECTORIES true "*") + +# this goes through all the directories and +# +# 1. add them if there is a CMakeLists.txt inside +# 2. add correspondings tests (for CTest) for BOTH x86 and x64 when possible +# +foreach(directory ${directories}) + if(NOT(IS_DIRECTORY ${directory})) + continue() + endif() + + if(NOT(EXISTS ${directory}/CMakeLists.txt)) + continue() + endif() + + add_subdirectory(${directory}) + + get_filename_component(dirname ${directory} NAME) + if((dirname STREQUAL "test_utils") OR (dirname STREQUAL "gtest_utils")) + continue() + endif() + + add_test(NAME ${dirname}_x64 + COMMAND ${USVFS_TEST_BINDIR}/${dirname}_x64.exe + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + ) + add_test(NAME ${dirname}_x86 + COMMAND ${USVFS_TEST_BINDIR}/${dirname}_x86.exe + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + ) +endforeach() diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/data/file.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/data/file.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/data/file.txt diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/docs/doc.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/docs/doc.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/docs/doc.txt diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/empty/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/empty/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/empty/.gitkeep diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/readme.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/readme.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/readme.txt diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/data/file.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/data/file.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/data/file.txt diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/docs/doc.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/docs/doc.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/docs/doc.txt diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/empty/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/empty/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/empty/.gitkeep diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/info.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/info.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/info.txt diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/readme.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/readme.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/readme.txt diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/data/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/data/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/data/.gitkeep diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/mods/mod1/docs/doc.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/mods/mod1/docs/doc.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/mods/mod1/docs/doc.txt diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/mods/mod1/docs/subdocs/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/mods/mod1/docs/subdocs/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/mods/mod1/docs/subdocs/.gitkeep diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/data/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/data/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/data/.gitkeep diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/mods/mod1/docs/doc.txt b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/mods/mod1/docs/doc.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/mods/mod1/docs/doc.txt diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/mods/mod1/docs/subdocs/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/mods/mod1/docs/subdocs/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/mods/mod1/docs/subdocs/.gitkeep diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/example.cpp b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/example.cpp new file mode 100644 index 0000000..9a79d16 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/example.cpp @@ -0,0 +1,139 @@ +#include <filesystem> +#include <fstream> + +std::filesystem::path storages_path; +std::filesystem::path hudpainter_path; +bool has_mo2; + +// See below main() for implementation. +void request_directory(std::filesystem::path); +std::filesystem::path restrict_path(std::filesystem::path); +std::vector<std::filesystem::path> get_files(); +std::string read_file(std::filesystem::path); +void write_file(std::filesystem::path); + +// Expected path when not using MO2 +int main() +{ + // RedFileSystem plugin (DLL) is loaded. + has_mo2 = GetModuleHandle(TEXT("usvfs_x64.dll")) != nullptr; + + // Setup paths + auto path = std::filesystem::absolute("."); // <GAME>\bin\x64 + auto game_path = path.parent_path().parent_path(); // <GAME> + + storages_path = game_path / "r6" / "storages"; // <GAME>\r6\storages + + // Create storages directory if it is no present. + request_directory(storages_path); // <GAME>\r6\storages + + // Game is running... + + // HUD Painter request its storage endpoint. + hudpainter_path = storages_path / "HUDPainter"; + request_directory(hudpainter_path); // <GAME>\r6\storages\HUDPainter + + // HUD Painter request a file (present in archive). + auto default_path = + restrict_path("DEFAULT.json"); // <GAME>\r6\storages\HUDPainter\DEFAULT.json + + // HUD Painter read file in `default_path` with success. + auto data = read_file(default_path); // <GAME>\r6\storages\HUDPainter\DEFAULT.json + + // HUD Painter request another file. + auto test_path = + restrict_path("TEST.json"); // <GAME>\r6\storages\HUDPainter\TEST.json + + // (Bug A) HUD Painter write file in `test_path`. + write_file(test_path); // <GAME>\r6\storages\HUDPainter\TEST.json + // file is created with workaround, not created otherwise. + + // (Bug B) HUD Painter request list of files. + // TEST.json is successfully created when using workaround. + auto files = get_files(); // [0] <GAME>\r6\storages\HUDPainter\DEFAULT.json + + // files.size() == 1 + + // Expect: + // files.size() == 2 + // + // [0] <GAME>\r6\storages\HUDPainter\DEFAULT.json + // [1] <GAME>\r6\storages\HUDPainter\TEST.json +} + +// Create directory if it is no present +void request_directory(std::filesystem::path path) +{ + bool is_present = std::filesystem::exists(path); + + if (is_present) { + return; + } + std::filesystem::create_directory(path); +} + +// Resolve path for HUDPainter (security layer) +std::filesystem::path restrict_path(std::filesystem::path path) +{ + auto real_path = std::filesystem::weakly_canonical(hudpainter_path / path); + + // Workaround when using MO2 + if (has_mo2) { + return real_path; + } + // Expected implementation without MO2 + if (real_path.string().find(hudpainter_path.string() + "\\") != 0) { + // "throw" error + } + return real_path; +} + +// List files in storage of HUDPainter. +std::vector<std::filesystem::path> get_files() +{ + std::vector<std::filesystem::path> files; + auto entries = std::filesystem::directory_iterator(hudpainter_path); + + for (const auto& entry : entries) { + if (entry.is_regular_file()) { + auto file_name = entry.path().filename(); + auto file_path = + hudpainter_path / + file_name; // Culprit of bug B. When using entry.path() directly, it works. + + files.emplace_back(file_path); + } + } + return files; +} + +std::string read_file(std::filesystem::path path) +{ + std::ifstream stream; + + // With workaround: + // std::filesystem::create_directories(path.parent_path()); + stream.open(path); + if (!stream.is_open()) { + return ""; + } + std::stringstream data; + + data << stream.rdbuf(); + stream.close(); + return data.str(); +} + +void write_file(std::filesystem::path path) +{ + std::ofstream stream; + + // With workaround: + // std::filesystem::create_directories(path.parent_path()); + stream.open(path, std::ios_base::trunc); + if (!stream.is_open()) { + return; + } + stream << "<DATA>"; + stream.close(); +} diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/data/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/data/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/data/.gitkeep diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/mods/HUD Painter/r6/storages/HUDPainter/DEFAULT.json b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/mods/HUD Painter/r6/storages/HUDPainter/DEFAULT.json new file mode 100644 index 0000000..dd85d0a --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/mods/HUD Painter/r6/storages/HUDPainter/DEFAULT.json @@ -0,0 +1,11 @@ +{ + "propertiesDefault": [ + { + "name": "MainColors.Red", + "red": 1.1761, + "green": 0.3809, + "blue": 0.3476, + "alpha": 1 + } + ] +} diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/overwrite/r6/storages/HUDPainter/TEST.json b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/overwrite/r6/storages/HUDPainter/TEST.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/overwrite/r6/storages/HUDPainter/TEST.json @@ -0,0 +1 @@ +{} diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/source/data/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/source/data/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/source/data/.gitkeep diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/source/mods/HUD Painter/r6/storages/HUDPainter/DEFAULT.json b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/source/mods/HUD Painter/r6/storages/HUDPainter/DEFAULT.json new file mode 100644 index 0000000..dd85d0a --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/source/mods/HUD Painter/r6/storages/HUDPainter/DEFAULT.json @@ -0,0 +1,11 @@ +{ + "propertiesDefault": [ + { + "name": "MainColors.Red", + "red": 1.1761, + "green": 0.3809, + "blue": 0.3476, + "alpha": 1 + } + ] +} diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/data/docs/doc.skip b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/data/docs/doc.skip new file mode 100644 index 0000000..09a24fd --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/data/docs/doc.skip @@ -0,0 +1 @@ +doc.skip in data/docs diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/data/file.txt b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/data/file.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/data/file.txt diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/docs/doc.skip b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/docs/doc.skip new file mode 100644 index 0000000..4b8bde6 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/docs/doc.skip @@ -0,0 +1 @@ +doc.skip in mod1/docs diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/docs/doc.txt b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/docs/doc.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/docs/doc.txt diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/empty/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/empty/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/empty/.gitkeep diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/readme.skip b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/readme.skip new file mode 100644 index 0000000..1d1857a --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/readme.skip @@ -0,0 +1 @@ +readme.skip in mod1 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/readme.txt b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/readme.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/readme.txt diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/overwrite/readme.skip b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/overwrite/readme.skip new file mode 100644 index 0000000..0062dba --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/overwrite/readme.skip @@ -0,0 +1 @@ +readme.skip in overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/data/docs/doc.skip b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/data/docs/doc.skip new file mode 100644 index 0000000..09a24fd --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/data/docs/doc.skip @@ -0,0 +1 @@ +doc.skip in data/docs diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/data/file.txt b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/data/file.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/data/file.txt diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/docs/doc.skip b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/docs/doc.skip new file mode 100644 index 0000000..4b8bde6 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/docs/doc.skip @@ -0,0 +1 @@ +doc.skip in mod1/docs diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/docs/doc.txt b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/docs/doc.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/docs/doc.txt diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/empty/.gitkeep b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/empty/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/empty/.gitkeep diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/readme.skip b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/readme.skip new file mode 100644 index 0000000..1d1857a --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/readme.skip @@ -0,0 +1 @@ +readme.skip in mod1 diff --git a/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/readme.txt b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/readme.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/readme.txt diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rcopyme4.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rcopyme4.txt new file mode 100644 index 0000000..d1e44e9 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rcopyme4.txt @@ -0,0 +1 @@ +rfolder\rcopyme4.txt original contents diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfile0.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfile0.txt new file mode 100644 index 0000000..9830306 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfile0.txt @@ -0,0 +1 @@ +rfolder\rfile0.txt original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfiledeletewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfiledeletewrite.txt new file mode 100644 index 0000000..2743678 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfiledeletewrite.txt @@ -0,0 +1 @@ +rfolder\rfiledeletewrite.txt overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfilerewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfilerewrite.txt new file mode 100644 index 0000000..bdff476 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfilerewrite.txt @@ -0,0 +1 @@ +rfolder\rfilerewrite.txt rewrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root0.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root0.txt new file mode 100644 index 0000000..a52b6be --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root0.txt @@ -0,0 +1 @@ +root0 original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root0w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root0w.txt new file mode 100644 index 0000000..c177584 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root0w.txt @@ -0,0 +1 @@ +root0w original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root1.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root1.txt new file mode 100644 index 0000000..06dc692 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root1.txt @@ -0,0 +1 @@ +root1 original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root1w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root1w.txt new file mode 100644 index 0000000..45172aa --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root1w.txt @@ -0,0 +1 @@ +root1w original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rcopyme4.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rcopyme4.txt new file mode 100644 index 0000000..d1e44e9 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rcopyme4.txt @@ -0,0 +1 @@ +rfolder\rcopyme4.txt original contents diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfile0.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfile0.txt new file mode 100644 index 0000000..9830306 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfile0.txt @@ -0,0 +1 @@ +rfolder\rfile0.txt original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfiledelete.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfiledelete.txt new file mode 100644 index 0000000..c19828f --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfiledelete.txt @@ -0,0 +1 @@ +rfolder\rfiledelete.txt original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfiledeletewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfiledeletewrite.txt new file mode 100644 index 0000000..93861cc --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfiledeletewrite.txt @@ -0,0 +1 @@ +rfolder\rfiledeletewrite.txt orignal file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfileoldname.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfileoldname.txt new file mode 100644 index 0000000..9376ea1 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfileoldname.txt @@ -0,0 +1 @@ +rfolder\rfileoldname.txt original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfilerewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfilerewrite.txt new file mode 100644 index 0000000..499775d --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfilerewrite.txt @@ -0,0 +1 @@ +rfolder\rfilerewrite.txt original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root0.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root0.txt new file mode 100644 index 0000000..a52b6be --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root0.txt @@ -0,0 +1 @@ +root0 original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root0w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root0w.txt new file mode 100644 index 0000000..c177584 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root0w.txt @@ -0,0 +1 @@ +root0w original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root1.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root1.txt new file mode 100644 index 0000000..06dc692 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root1.txt @@ -0,0 +1 @@ +root1 original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root1w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root1w.txt new file mode 100644 index 0000000..45172aa --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/mount/root1w.txt @@ -0,0 +1 @@ +root1w original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder1/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder1/mfile.txt new file mode 100644 index 0000000..227256a --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder1/mfile.txt @@ -0,0 +1 @@ +mfolder1\mfile.txt by mod1 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder1/mfilew.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder1/mfilew.txt new file mode 100644 index 0000000..420589d --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder1/mfilew.txt @@ -0,0 +1 @@ +mfolder1\mfilew.txt by mod1 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder3/mdeep3/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder3/mdeep3/mfile.txt new file mode 100644 index 0000000..6a7a0c2 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder3/mdeep3/mfile.txt @@ -0,0 +1 @@ +mfolder3\mdeep3\mfile.txt by mod1 (to be hidden by mod3) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mod1.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mod1.txt new file mode 100644 index 0000000..7d5dc0e --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mod1.txt @@ -0,0 +1 @@ +hello mod1 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mod1w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mod1w.txt new file mode 100644 index 0000000..1838cdf --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mod1w.txt @@ -0,0 +1 @@ +hello mod1w diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder2/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder2/mfile.txt new file mode 100644 index 0000000..8ff096e --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder2/mfile.txt @@ -0,0 +1 @@ +mfolder2\mfile.txt by mod2 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfile.txt new file mode 100644 index 0000000..c0f0f2c --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfile.txt @@ -0,0 +1 @@ +mfolder4\mfile.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletemove.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletemove.txt new file mode 100644 index 0000000..e35f1f3 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletemove.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletemove.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletemove2p.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletemove2p.txt new file mode 100644 index 0000000..cc572dc --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletemove2p.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletemove2p.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletewrite.txt new file mode 100644 index 0000000..f699360 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletewrite.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletewrite.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletewrite2p.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletewrite2p.txt new file mode 100644 index 0000000..9eae9c5 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletewrite2p.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletewrite2p.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfilemoveover.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfilemoveover.txt new file mode 100644 index 0000000..88a6d08 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfilemoveover.txt @@ -0,0 +1 @@ +mfolder4/mfilemoveover.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfileoverwrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfileoverwrite.txt new file mode 100644 index 0000000..61b92a6 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfileoverwrite.txt @@ -0,0 +1 @@ +mfolder4\mfileoverwrite.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfilerewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfilerewrite.txt new file mode 100644 index 0000000..727a48f --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfilerewrite.txt @@ -0,0 +1 @@ +mfolder4\mfilerewrite.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mod2.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mod2.txt new file mode 100644 index 0000000..49bbbf1 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mod2.txt @@ -0,0 +1 @@ +hello mod2 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mfolder3/mdeep3/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mfolder3/mdeep3/mfile.txt new file mode 100644 index 0000000..fac8213 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mfolder3/mdeep3/mfile.txt @@ -0,0 +1 @@ +mfolder3\mdeep3\mfile.txt by mod3 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mfolder4/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mfolder4/mfile.txt new file mode 100644 index 0000000..5e6ca00 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mfolder4/mfile.txt @@ -0,0 +1 @@ +mfolder4\mfile.txt by mod3 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mod3.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mod3.txt new file mode 100644 index 0000000..47fbacd --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mod3.txt @@ -0,0 +1 @@ +hello mod3 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfile.txt new file mode 100644 index 0000000..c67ecfe --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfile.txt @@ -0,0 +1 @@ +mfolder4\mfile.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfiledeletemove.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfiledeletemove.txt new file mode 100644 index 0000000..00cc0cd --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfiledeletemove.txt @@ -0,0 +1 @@ +mfolder4\mfiledeletemove.txt overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfiledeletewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfiledeletewrite.txt new file mode 100644 index 0000000..083a6b8 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfiledeletewrite.txt @@ -0,0 +1 @@ +mfolder4\mfiledeletewrite.txt overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfilemoveover.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfilemoveover.txt new file mode 100644 index 0000000..a8a93c2 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfilemoveover.txt @@ -0,0 +1 @@ +mfolder4\mfilemoveover.txt overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfileoverwrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfileoverwrite.txt new file mode 100644 index 0000000..fc6d42f --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfileoverwrite.txt @@ -0,0 +1 @@ +mfolder4\mfileoverwrite.txt overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfilerewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfilerewrite.txt new file mode 100644 index 0000000..a3b8ced --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfilerewrite.txt @@ -0,0 +1 @@ +mfolder4\mfilerewrite.txt rewrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/.empty b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/.empty new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/.empty diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder1/newfile1.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder1/newfile1.txt new file mode 100644 index 0000000..bf76f1d --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder1/newfile1.txt @@ -0,0 +1 @@ +newfile1.txt nonrecursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder1/newfolder1/newfile1.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder1/newfolder1/newfile1.txt new file mode 100644 index 0000000..4802577 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder1/newfolder1/newfile1.txt @@ -0,0 +1 @@ +newfile1.txt nonrecursive overwrite subfolder diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder2/newfile2.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder2/newfile2.txt new file mode 100644 index 0000000..a4fa7fc --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder2/newfile2.txt @@ -0,0 +1 @@ +newfile2.txt recursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder3/newfolder3/newfile3.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder3/newfolder3/newfile3.txt new file mode 100644 index 0000000..933eb0c --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder3/newfolder3/newfile3.txt @@ -0,0 +1 @@ +newfile3.txt recursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder3/newfolder3/newfile3e.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder3/newfolder3/newfile3e.txt new file mode 100644 index 0000000..5e5286a --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder3/newfolder3/newfile3e.txt @@ -0,0 +1 @@ +newfile3e.txt recursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/mfiledeletemove2p.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/mfiledeletemove2p.txt new file mode 100644 index 0000000..6b85a92 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/mfiledeletemove2p.txt @@ -0,0 +1 @@ +mfolder4\mfiledeletemove2p.txt overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/mfiledeletewrite2p.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/mfiledeletewrite2p.txt new file mode 100644 index 0000000..64cac84 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/mfiledeletewrite2p.txt @@ -0,0 +1 @@ +mfolder4\mfiledeletewrite2p.txt overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4.txt new file mode 100644 index 0000000..cba5361 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4.txt @@ -0,0 +1 @@ +newfile4.txt recursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4e.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4e.txt new file mode 100644 index 0000000..eb90089 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4e.txt @@ -0,0 +1 @@ +newfile4e.txt recursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4enr.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4enr.txt new file mode 100644 index 0000000..f8b5f9f --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4enr.txt @@ -0,0 +1 @@ +newfile4enr.txt nonrecursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/epnewfile4.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/epnewfile4.txt new file mode 100644 index 0000000..06edab8 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/epnewfile4.txt @@ -0,0 +1 @@ +epnewfile4.txt nonrecursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/epnewfile4r.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/epnewfile4r.txt new file mode 100644 index 0000000..8fd183d --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/epnewfile4r.txt @@ -0,0 +1 @@ +epnewfile4r.txt recursive overwrite diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4p/rcopyme4.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4p/rcopyme4.txt new file mode 100644 index 0000000..d1e44e9 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4p/rcopyme4.txt @@ -0,0 +1 @@ +rfolder\rcopyme4.txt original contents diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/rcopyme4.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/rcopyme4.txt new file mode 100644 index 0000000..d1e44e9 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/rcopyme4.txt @@ -0,0 +1 @@ +rfolder\rcopyme4.txt original contents diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/rfolder/rfilenewname.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/rfolder/rfilenewname.txt new file mode 100644 index 0000000..9376ea1 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/rfolder/rfilenewname.txt @@ -0,0 +1 @@ +rfolder\rfileoldname.txt original file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root1.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root1.txt new file mode 100644 index 0000000..5aa129b --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root1.txt @@ -0,0 +1 @@ +root1 source file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root1w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root1w.txt new file mode 100644 index 0000000..0c04aa5 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root1w.txt @@ -0,0 +1 @@ +root1w source file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root2.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root2.txt new file mode 100644 index 0000000..4ac40bb --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root2.txt @@ -0,0 +1 @@ +root2 source file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root2w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root2w.txt new file mode 100644 index 0000000..83ddb42 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root2w.txt @@ -0,0 +1 @@ +root2w source file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder1/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder1/mfile.txt new file mode 100644 index 0000000..227256a --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder1/mfile.txt @@ -0,0 +1 @@ +mfolder1\mfile.txt by mod1 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder1/mfilew.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder1/mfilew.txt new file mode 100644 index 0000000..420589d --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder1/mfilew.txt @@ -0,0 +1 @@ +mfolder1\mfilew.txt by mod1 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder3/mdeep3/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder3/mdeep3/mfile.txt new file mode 100644 index 0000000..6a7a0c2 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder3/mdeep3/mfile.txt @@ -0,0 +1 @@ +mfolder3\mdeep3\mfile.txt by mod1 (to be hidden by mod3) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mod1.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mod1.txt new file mode 100644 index 0000000..7d5dc0e --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mod1.txt @@ -0,0 +1 @@ +hello mod1 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mod1w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mod1w.txt new file mode 100644 index 0000000..1838cdf --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mod1w.txt @@ -0,0 +1 @@ +hello mod1w diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder2/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder2/mfile.txt new file mode 100644 index 0000000..8ff096e --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder2/mfile.txt @@ -0,0 +1 @@ +mfolder2\mfile.txt by mod2 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfile.txt new file mode 100644 index 0000000..c0f0f2c --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfile.txt @@ -0,0 +1 @@ +mfolder4\mfile.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletemove.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletemove.txt new file mode 100644 index 0000000..e35f1f3 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletemove.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletemove.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletemove2p.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletemove2p.txt new file mode 100644 index 0000000..cc572dc --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletemove2p.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletemove2p.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletewrite.txt new file mode 100644 index 0000000..f699360 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletewrite.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletewrite.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletewrite2p.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletewrite2p.txt new file mode 100644 index 0000000..9eae9c5 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletewrite2p.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletewrite2p.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfilemoveover.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfilemoveover.txt new file mode 100644 index 0000000..88a6d08 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfilemoveover.txt @@ -0,0 +1 @@ +mfolder4/mfilemoveover.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfileoverwrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfileoverwrite.txt new file mode 100644 index 0000000..61b92a6 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfileoverwrite.txt @@ -0,0 +1 @@ +mfolder4\mfileoverwrite.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfilerewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfilerewrite.txt new file mode 100644 index 0000000..727a48f --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfilerewrite.txt @@ -0,0 +1 @@ +mfolder4\mfilerewrite.txt by mod2 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mod2.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mod2.txt new file mode 100644 index 0000000..49bbbf1 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mod2.txt @@ -0,0 +1 @@ +hello mod2 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mfolder3/mdeep3/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mfolder3/mdeep3/mfile.txt new file mode 100644 index 0000000..fac8213 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mfolder3/mdeep3/mfile.txt @@ -0,0 +1 @@ +mfolder3\mdeep3\mfile.txt by mod3 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mfolder4/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mfolder4/mfile.txt new file mode 100644 index 0000000..5e6ca00 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mfolder4/mfile.txt @@ -0,0 +1 @@ +mfolder4\mfile.txt by mod3 (to be hidden by mod4) diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mod3.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mod3.txt new file mode 100644 index 0000000..47fbacd --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mod3.txt @@ -0,0 +1 @@ +hello mod3 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfile.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfile.txt new file mode 100644 index 0000000..c67ecfe --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfile.txt @@ -0,0 +1 @@ +mfolder4\mfile.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletemove.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletemove.txt new file mode 100644 index 0000000..657f07d --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletemove.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletemove.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletemove2p.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletemove2p.txt new file mode 100644 index 0000000..cfd08dd --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletemove2p.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletemove2p.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletewrite.txt new file mode 100644 index 0000000..a17d129 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletewrite.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletewrite.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletewrite2p.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletewrite2p.txt new file mode 100644 index 0000000..b198d1c --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletewrite2p.txt @@ -0,0 +1 @@ +mfolder4/mfiledeletewrite2p.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfilemoveover.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfilemoveover.txt new file mode 100644 index 0000000..b395908 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfilemoveover.txt @@ -0,0 +1 @@ +mfolder4/mfilemoveover.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfileoverwrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfileoverwrite.txt new file mode 100644 index 0000000..405c0c1 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfileoverwrite.txt @@ -0,0 +1 @@ +mfolder4\mfileoverwrite.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfilerewrite.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfilerewrite.txt new file mode 100644 index 0000000..def973c --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfilerewrite.txt @@ -0,0 +1 @@ +mfolder4\mfilerewrite.txt by mod4 diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/overwrite/.empty b/libs/usvfs/test/fixtures/usvfs_test/basic/source/overwrite/.empty new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/overwrite/.empty diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/root1.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/root1.txt new file mode 100644 index 0000000..5aa129b --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/root1.txt @@ -0,0 +1 @@ +root1 source file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/root1w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/root1w.txt new file mode 100644 index 0000000..0c04aa5 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/root1w.txt @@ -0,0 +1 @@ +root1w source file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/root2.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/root2.txt new file mode 100644 index 0000000..4ac40bb --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/root2.txt @@ -0,0 +1 @@ +root2 source file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/source/root2w.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/source/root2w.txt new file mode 100644 index 0000000..83ddb42 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/source/root2w.txt @@ -0,0 +1 @@ +root2w source file diff --git a/libs/usvfs/test/fixtures/usvfs_test/basic/vfs_mappings.txt b/libs/usvfs/test/fixtures/usvfs_test/basic/vfs_mappings.txt new file mode 100644 index 0000000..cc755b4 --- /dev/null +++ b/libs/usvfs/test/fixtures/usvfs_test/basic/vfs_mappings.txt @@ -0,0 +1,26 @@ +# mapdir <optional relative path under mount> +# <relative path under source to mount> +# ... +mapdir + mod1 + mod2 + mod3 + mod4 + +# mapdircreate <optional relative path under mount> +# <relative path under source to mount> +# ... +mapdircreate + overwrite + +# mapfile <optional relative path under mount> +# <relative path under source to mount> +# ... +mapfile root1.txt + root1.txt +mapfile root1w.txt + root1w.txt +mapfile root2.txt + root2.txt +mapfile root2w.txt + root2w.txt diff --git a/libs/usvfs/test/gtest_utils/CMakeLists.txt b/libs/usvfs/test/gtest_utils/CMakeLists.txt new file mode 100644 index 0000000..8dcda70 --- /dev/null +++ b/libs/usvfs/test/gtest_utils/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(GTest CONFIG REQUIRED) + +add_library(gtest_utils STATIC + gtest_utils.cpp + gtest_utils.h +) +set_target_properties(gtest_utils PROPERTIES FOLDER tests) +target_link_libraries(gtest_utils PUBLIC GTest::gtest) +target_include_directories(gtest_utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/libs/usvfs/test/gtest_utils/gtest_utils.cpp b/libs/usvfs/test/gtest_utils/gtest_utils.cpp new file mode 100644 index 0000000..f292dfd --- /dev/null +++ b/libs/usvfs/test/gtest_utils/gtest_utils.cpp @@ -0,0 +1,122 @@ +#include "gtest_utils.h" + +// this file is shared by both usvfs_global_test and usvfs_global_test_runner +// + +#include <filesystem> +#include <format> +#include <fstream> + +#include <boost/algorithm/string.hpp> + +#include <gtest/gtest.h> + +std::string trimmed(std::string content) +{ + boost::algorithm::trim(content); + return content; +} + +// small utility to read files +std::optional<std::string> read_content(const std::filesystem::path& path, + bool trim = true) +{ + std::ifstream ifs(path, std::ios::binary | std::ios::ate); + + if (!ifs) { + return {}; + } + + const auto count = ifs.tellg(); + + std::string buffer(static_cast<std::size_t>(count), '\0'); + + ifs.seekg(0, std::ios::beg); + ifs.read(buffer.data(), count); + + if (trim) { + boost::algorithm::trim(buffer); + } + + return buffer; +} + +::testing::AssertionResult AssertDirectoryEquals(const std::filesystem::path& expected, + const std::filesystem::path& actual, + bool content) +{ + std::vector<std::string> failure_messages; + std::vector<std::filesystem::path> in_both; + + // iterate left, check on right + for (const auto& it : std::filesystem::recursive_directory_iterator{expected}) { + const auto relpath = relative(it.path(), expected); + if (!exists(actual / relpath)) { + failure_messages.push_back( + std::format("{} expected but not found", relpath.string())); + } else { + in_both.push_back(relpath); + } + } + + // iterate right, check on left + for (const auto& it : std::filesystem::recursive_directory_iterator{actual}) { + const auto relpath = relative(it.path(), actual); + if (!exists(expected / relpath)) { + failure_messages.push_back( + std::format("{} found but not expected", relpath.string())); + } + } + + // check contents + if (content) { + for (const auto& relpath : in_both) { + const auto expected_path = expected / relpath, actual_path = actual / relpath; + + if (is_directory(expected_path) != is_directory(actual_path)) { + failure_messages.push_back( + std::format("{} type mismatch, expected {} but found {}", relpath.string(), + is_directory(expected_path) ? "directory" : "file", + is_directory(expected_path) ? "file" : "directory")); + continue; + } + + if (is_directory(expected_path)) { + continue; + } + + if (read_content(expected_path) != read_content(actual_path)) { + failure_messages.push_back( + std::format("{} content mismatch", relpath.string())); + } + } + } + + if (failure_messages.empty()) { + return ::testing::AssertionSuccess(); + } + + return ::testing::AssertionFailure() + << "\n" + << boost::algorithm::join(failure_messages, "\n") << "\n"; +} + +::testing::AssertionResult AssertContentEquals(std::string_view expected, + const std::filesystem::path& path, + bool trim) +{ + const auto content = read_content(path, trim); + + if (!content) { + return ::testing::AssertionFailure() + << "failed to open path '" << path.string() << "'"; + } + + if (*content != expected) { + return ::testing::AssertionFailure() + << "mismatch content for '" << path.string() << "', expected '" << expected + << "', found '" << *content << "'"; + } + + return ::testing::AssertionSuccess(); +} diff --git a/libs/usvfs/test/gtest_utils/gtest_utils.h b/libs/usvfs/test/gtest_utils/gtest_utils.h new file mode 100644 index 0000000..1dba70c --- /dev/null +++ b/libs/usvfs/test/gtest_utils/gtest_utils.h @@ -0,0 +1,29 @@ +#pragma once + +// this file is shared by both usvfs_global_test and usvfs_global_test_runner +// + +#include <filesystem> + +#include <gtest/gtest.h> + +::testing::AssertionResult AssertDirectoryEquals(const std::filesystem::path& expected, + const std::filesystem::path& actual, + bool content = true); + +::testing::AssertionResult AssertContentEquals(std::string_view expected, + const std::filesystem::path& path, + bool trim = true); + +// macro to assert that the contents of two directories are identical - directories are +// compared recursively and file contents are compared (excluding extra spaces or lines +// in file) +// +#define ASSERT_DIRECTORY_EQ(Expected, Actual) \ + ASSERT_TRUE(AssertDirectoryEquals(Expected, Actual)) + +// macro to assert that the contents of two files are identical (excluding extra space +// or lines in file) +// +#define ASSERT_CONTENT_EQ(Expected, Path) \ + ASSERT_TRUE(AssertContentEquals(Expected, Path)) diff --git a/libs/usvfs/test/shared_test/CMakeLists.txt b/libs/usvfs/test/shared_test/CMakeLists.txt new file mode 100644 index 0000000..c5a42ea --- /dev/null +++ b/libs/usvfs/test/shared_test/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(GTest CONFIG REQUIRED) + +add_executable(shared_test main.cpp) +usvfs_set_test_properties(shared_test) +target_link_libraries(shared_test PRIVATE test_utils GTest::gtest GTest::gtest_main) diff --git a/libs/usvfs/test/shared_test/main.cpp b/libs/usvfs/test/shared_test/main.cpp new file mode 100644 index 0000000..c6b5be6 --- /dev/null +++ b/libs/usvfs/test/shared_test/main.cpp @@ -0,0 +1,248 @@ +#include <boost/interprocess/containers/string.hpp> +#include <boost/interprocess/shared_memory_object.hpp> +#include <boost/predef.h> +#include <gtest/gtest.h> +#include <shared_memory.h> +#include <spdlog/sinks/stdout_sinks.h> +#include <wildcard.h> +#include <windows_sane.h> + +#define PRIVATE public +#include <directory_tree.h> +#undef PRIVATE + +#include <tree_container.h> + +using namespace usvfs::shared; + +using namespace boost::interprocess; + +static const char g_SHMName[] = "treetest_shm"; +static const char TreeName[] = "treetest_tree"; + +typedef DirectoryTree<int> TreeType; +typedef TreeContainer<TreeType> ContainerType; + +typedef boost::container::scoped_allocator_adaptor< + boost::interprocess::allocator<void, SegmentManagerT>> + VoidAllocator; +typedef VoidAllocator::rebind<char>::other CharAllocator; +typedef basic_string<char, std::char_traits<char>, CharAllocator> SHMString; +typedef DirectoryTree<SHMString> ComplexTreeType; +typedef TreeContainer<ComplexTreeType> ComplexContainerType; + +template <> +struct usvfs::shared::SHMDataCreator<int, int> +{ + static int create(int source, const VoidAllocatorT&) { return source; } +}; + +template <> +inline int usvfs::shared::createDataEmpty<int>(const typename VoidAllocatorT&) +{ + return 0; +} + +template <> +inline void usvfs::shared::dataAssign<int>(int& destination, const int& source) +{ + destination = source; +} + +template <> +inline SHMString +usvfs::shared::createDataEmpty<SHMString>(const typename VoidAllocatorT& allocator) +{ + return SHMString("", allocator); +} + +template <> +inline void usvfs::shared::dataAssign<SHMString>(SHMString& destination, + const SHMString& source) +{ + destination.assign(source.c_str()); +} + +static std::shared_ptr<spdlog::logger> logger() +{ + std::shared_ptr<spdlog::logger> result = spdlog::get("test"); + if (result.get() == nullptr) { + result = spdlog::stdout_logger_mt("test"); + } + return result; +} + +TEST(WildcardTest, MatchWildcard) +{ + EXPECT_TRUE(wildcard::Match(TEXT("abc"), TEXT("a*"))); + EXPECT_TRUE(wildcard::Match(TEXT("abc"), TEXT("a*c"))); + EXPECT_TRUE(wildcard::Match(TEXT("abc"), TEXT("a?c"))); + EXPECT_TRUE(wildcard::Match(TEXT("abc"), TEXT("abc"))); + EXPECT_TRUE(wildcard::Match(TEXT("abc"), TEXT("abc*"))); + + EXPECT_TRUE(wildcard::Match("abc", "*.*")); + EXPECT_TRUE(wildcard::Match("abc.def", "*")); + EXPECT_TRUE(wildcard::Match(TEXT("abc"), TEXT("*.*"))); + EXPECT_TRUE(wildcard::Match(TEXT("abc.def"), TEXT("*"))); + + EXPECT_NE(nullptr, wildcard::PartialMatch("abc", "*.*")); + EXPECT_NE(nullptr, wildcard::PartialMatch("abc.def", "*")); + EXPECT_EQ('\0', *wildcard::PartialMatch("abc", "*.*")); + EXPECT_EQ('\0', *wildcard::PartialMatch("abc.def", "*")); + + EXPECT_FALSE(wildcard::Match(TEXT("abc"), TEXT("b*"))); +} + +TEST(DirectoryTreeTest, SimpleTreeInit) +{ + EXPECT_NO_THROW({ + ContainerType tree(g_SHMName, 4096); + TreeType::NodePtrT p = tree.addFile(R"(C:\temp\test.txt)", 42, false); + EXPECT_NE(TreeType::NodePtrT(), p); + }); +} + +TEST(DirectoryTreeTest, FindNode) +{ + shared_memory_object::remove(g_SHMName); + ContainerType tree(g_SHMName, 64 * 1024); + EXPECT_NE(nullptr, tree.addFile(R"(C:\temp\bla)", 0x42, 0, false)); + + EXPECT_NE(nullptr, tree->findNode(R"(C:\temp)").get()); + EXPECT_EQ(nullptr, tree->findNode(R"(C:\temp\bla\blubb)").get()); +} + +struct TestVisitor +{ + TreeType::NodePtrT lastNode; + bool flag40{false}; + + void operator()(const TreeType::NodePtrT& node) + { + lastNode = node; + flag40 = node->hasFlag(0x40); + logger()->debug("{0} - {1}", lastNode->name(), flag40); + // BOOST_LOG_SEV(globalLogger::get(), LogLevel::Debug) << lastNode->name() << " - " + // << flag40; + } +}; + +TEST(DirectoryTreeTest, VisitPath) +{ + shared_memory_object::remove(g_SHMName); + ContainerType tree(g_SHMName, 64 * 1024); + EXPECT_NE(nullptr, tree.addFile(R"(C:\temp\bla)", 1, 0x40, false)); + + TestVisitor visitor; + + tree->visitPath(R"(C:\temp\bla\blubb)", + TreeType::VisitorFunction([&](const TreeType::NodePtrT& node) { + visitor(node); + })); + EXPECT_TRUE(visitor.flag40); + EXPECT_EQ("bla", visitor.lastNode->name()); +} + +TEST(DirectoryTreeTest, WildCardFind) +{ + shared_memory_object::remove(g_SHMName); + EXPECT_NO_THROW({ + ContainerType tree(g_SHMName, 64 * 1024); + + EXPECT_NE(nullptr, tree.addFile(R"(C:\temp)", 1, FLAG_DIRECTORY, false)); + EXPECT_NE(nullptr, tree.addFile(R"(C:\temp\abc)", 1, 0, false)); + EXPECT_NE(nullptr, tree.addFile(R"(C:\temp\abd)", 2, 0, false)); + EXPECT_NE(nullptr, tree.addFile(R"(C:\temp\ace)", 3, 0, false)); + + EXPECT_EQ(3, tree->find(R"(C:\temp\*)").size()); + EXPECT_NE(nullptr, tree->node("C:")); + EXPECT_EQ(1, tree->node("C:")->find("*").size()); + EXPECT_NE(nullptr, tree->node("C:")->node("temp")); + EXPECT_EQ(3, tree->node("C:")->node("temp")->find("*").size()); // alternative + // search on the top-level + EXPECT_EQ(1, tree->find("*").size()); + // * should work + EXPECT_EQ(2, tree->find(R"(C:\temp\ab*)").size()); + // * does not match directory separators + EXPECT_EQ(0, tree->find("*ab*").size()); + // matches only the directory itself + EXPECT_EQ(1, tree->find(R"(C:\temp*)").size()); + }); +} + +TEST(DirectoryTreeTest, SHMAllocation) +{ + EXPECT_NO_THROW({ + ContainerType create(g_SHMName, 64 * 1024); + + { // creation + create.addFile(R"(C:\temp\abc)", 1, false); + create.addFile(R"(C:\temp\abd)", 2, false); + create.addFile(R"(C:\temp\ace)", 3, false); + } + + { // access + ContainerType access(g_SHMName, 64 * 1024); + EXPECT_NE(nullptr, access.get()); + std::vector<TreeType::NodePtrT> res = access->find(R"(C:\temp\*)"); + EXPECT_EQ(3, res.size()); // matches the three files + EXPECT_EQ(access->m_Self.lock().get(), access->node("C:")->parent().get()); + } + }); +} + +TEST(DirectoryTreeTest, SHMAllocationError) +{ + EXPECT_NO_THROW({ + try { + ContainerType tree(g_SHMName, 4096); + int c = 0; + for (char i = 'a'; i <= 'z'; ++i) { + for (char j = 'a'; j <= 'z'; ++j) { + std::string name = std::string(R"(C:\temp\)") + i + j; + tree.addFile(name, ++c, false); + } + } + + EXPECT_EQ(1, tree->node("C:")->node("temp")->node("aa", MissingThrow)->data()); + EXPECT_EQ(26, tree->node("C:")->node("temp")->node("az", MissingThrow)->data()); + } catch (const std::exception& e) { + logger()->error("{0}", e.what()); + // BOOST_LOG_SEV(globalLogger::get(), LogLevel::Error) << e.what(); + throw; + } + }); +} + +TEST(DirectoryTreeTest, SHMAllocationErrorComplex) +{ + EXPECT_NO_THROW({ + try { + ComplexContainerType tree(g_SHMName, 4096); + SHMString str = tree.create("gaga"); + for (char i = 'a'; i <= 'z'; ++i) { + for (char j = 'a'; j <= 'z'; ++j) { + std::string name = std::string(R"(C:\temp\)") + i + j; + tree.addFile(name, str, false); + } + } + EXPECT_STREQ( + str.c_str(), + tree->node("C:")->node("temp")->node("aa", MissingThrow)->data().c_str()); + EXPECT_STREQ( + str.c_str(), + tree->node("C:")->node("temp")->node("az", MissingThrow)->data().c_str()); + } catch (const std::exception& e) { + logger()->error("{}", e.what()); + throw; + } + }); +} + +int main(int argc, char** argv) +{ + auto logger = spdlog::stdout_logger_mt("usvfs"); + logger->set_level(spdlog::level::warn); + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/libs/usvfs/test/test_utils/CMakeLists.txt b/libs/usvfs/test/test_utils/CMakeLists.txt new file mode 100644 index 0000000..88c75d6 --- /dev/null +++ b/libs/usvfs/test/test_utils/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(GTest CONFIG REQUIRED) + +add_library(test_utils STATIC + test_helpers.cpp + test_helpers.h +) +set_target_properties(test_utils PROPERTIES FOLDER tests) +target_link_libraries(test_utils PUBLIC shared) +target_include_directories(test_utils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/libs/usvfs/test/test_utils/test_helpers.cpp b/libs/usvfs/test/test_utils/test_helpers.cpp new file mode 100644 index 0000000..2daf3bb --- /dev/null +++ b/libs/usvfs/test/test_utils/test_helpers.cpp @@ -0,0 +1,248 @@ +#pragma once + +#include <format> + +#include "test_helpers.h" +#include "winapi.h" + +namespace test +{ + +std::string FuncFailed::msg(std::string_view func, const char* arg1, + const unsigned long* res, const char* what) +{ + std::string buffer; + buffer.reserve(128); + + std::format_to(std::back_inserter(buffer), "{}() {}", func, what ? what : "failed"); + const char* sep = " : "; + if (arg1) { + std::format_to(std::back_inserter(buffer), "{}{}", sep, arg1); + sep = ", "; + } + if (res) { + std::format_to(std::back_inserter(buffer), "{}result = {} ({:#x})", sep, *res, + *res); + } + return buffer; +} + +ScopedFILE ScopedFILE::open(const std::filesystem::path& filepath, + std::wstring_view mode, errno_t& err) +{ + FILE* fp = nullptr; + err = _wfopen_s(&fp, filepath.c_str(), mode.data()); + if (err || !fp) { + return ScopedFILE(); + } + return ScopedFILE(fp); +} + +ScopedFILE ScopedFILE::open(const std::filesystem::path& filepath, + std::wstring_view mode) +{ + errno_t err; + auto file = open(filepath, mode, err); + if (err || !file) { + throw_testWinFuncFailed("_wfopen_s", filepath.string().c_str(), err); + } + return file; +} + +path path_of_test_bin(const path& relative) +{ + path base(winapi::wide::getModuleFileName(nullptr)); + return relative.empty() ? base.parent_path() : base.parent_path() / relative; +} + +path path_of_test_temp(const path& relative) +{ + return path_of_test_bin().parent_path() / "temp" / relative; +} + +path path_of_test_fixtures(const path& relative) +{ + return path_of_test_bin().parent_path() / "fixtures" / relative; +} + +path path_of_usvfs_lib(const path& relative) +{ + return path_of_test_bin().parent_path().parent_path() / "lib" / relative; +} + +std::string platform_dependant_executable(const char* name, const char* ext, + const char* platform) +{ + std::string res = name; + res += "_"; + if (platform) + res += platform; + else +#if _WIN64 + res += "x64"; +#else + res += "x86"; +#endif + res += "."; + res += ext; + return res; +} + +path path_as_relative(const path& base, const path& full_path) +{ + auto rel_begin = full_path.begin(); + auto base_iter = base.begin(); + while (rel_begin != full_path.end() && base_iter != base.end() && + *rel_begin == *base_iter) { + ++rel_begin; + ++base_iter; + } + + if (base_iter != base.end()) // full_path is not a sub-folder of base + return full_path; + + if (rel_begin == full_path.end()) // full_path == base + return path(L"."); + + // full_path is a sub-folder of base so take only relative path + path result; + for (; rel_begin != full_path.end(); ++rel_begin) + result /= *rel_begin; + return result; +} + +std::vector<char> read_small_file(const path& file, bool binary) +{ + using namespace std; + + const auto f = ScopedFILE::open(file, binary ? L"rb" : L"rt"); + + if (fseek(f, 0, SEEK_END)) + throw_testWinFuncFailed("fseek", (unsigned long)0); + + long size = ftell(f); + if (size < 0) + throw_testWinFuncFailed("ftell", (unsigned long)size); + if (size > 0x10000000) // sanity check limit to 256M + throw test::FuncFailed("read_small_file", "file size too large", + (unsigned long)size); + + if (fseek(f, 0, SEEK_SET)) + throw_testWinFuncFailed("fseek", (unsigned long)0); + + std::vector<char> content(static_cast<size_t>(size)); + content.resize(fread(content.data(), sizeof(char), content.size(), f)); + + return content; +} + +bool compare_files(const path& file1, const path& file2, bool binary) +{ + // TODO: if this is ever used for big file should read files in chunks + return read_small_file(file1, binary) == read_small_file(file2, binary); +} + +bool is_empty_folder(const path& dpath, bool or_doesnt_exist) +{ + bool isDir = false; + if (!winapi::ex::wide::fileExists(dpath.c_str(), &isDir)) + return or_doesnt_exist; + + if (!isDir) + return false; + + for (const auto& f : winapi::ex::wide::quickFindFiles(dpath.c_str(), L"*")) + if (f.fileName != L"." && f.fileName != L"..") + return false; + return true; +} + +void delete_file(const path& file) +{ + if (!DeleteFileW(file.c_str())) { + auto err = GetLastError(); + if (err != ERROR_FILE_NOT_FOUND && err != ERROR_PATH_NOT_FOUND) + throw_testWinFuncFailed("DeleteFile", file.string()); + } +} + +void recursive_delete_files(const path& dpath) +{ + bool isDir = false; + if (!winapi::ex::wide::fileExists(dpath.c_str(), &isDir)) + return; + if (!isDir) + delete_file(dpath); + else { + // dpath exists and its a directory: + std::vector<std::wstring> recurse; + for (const auto& f : winapi::ex::wide::quickFindFiles(dpath.c_str(), L"*")) { + if (f.fileName == L"." || f.fileName == L"..") + continue; + if (f.attributes & FILE_ATTRIBUTE_DIRECTORY) + recurse.push_back(f.fileName); + else + delete_file(dpath / f.fileName); + } + for (auto r : recurse) + recursive_delete_files(dpath / r); + if (!RemoveDirectoryW(dpath.c_str())) + throw_testWinFuncFailed("RemoveDirectory", dpath.string().c_str()); + } + if (winapi::ex::wide::fileExists(dpath.c_str())) + throw FuncFailed("delete_directory_tree", dpath.string().c_str()); +} + +void recursive_copy_files(const path& src_path, const path& dest_path, bool overwrite) +{ + bool srcIsDir = false, destIsDir = false; + if (!winapi::ex::wide::fileExists(src_path.c_str(), &srcIsDir)) + throw FuncFailed("recursive_copy", "source doesn't exist", + src_path.string().c_str()); + if (!winapi::ex::wide::fileExists(dest_path.c_str(), &destIsDir) && srcIsDir) { + winapi::ex::wide::createPath(dest_path.c_str()); + destIsDir = true; + } + + if (!srcIsDir) + if (!destIsDir) { + if (!CopyFileW(src_path.c_str(), dest_path.c_str(), overwrite)) + throw_testWinFuncFailed( + "CopyFile", (src_path.string() + " => " + dest_path.string()).c_str()); + return; + } else + throw FuncFailed("recursive_copy", + "source is a file but destination is a directory", + (src_path.string() + ", " + dest_path.string()).c_str()); + + if (!destIsDir) + throw FuncFailed("recursive_copy", + "source is a directory but destination is a file", + (src_path.string() + ", " + dest_path.string()).c_str()); + + // source and destination are both directories: + std::vector<std::wstring> recurse; + for (const auto& f : winapi::ex::wide::quickFindFiles(src_path.c_str(), L"*")) { + if (f.fileName == L"." || f.fileName == L"..") + continue; + if (f.attributes & FILE_ATTRIBUTE_DIRECTORY) + recurse.push_back(f.fileName); + else if (!CopyFileW((src_path / f.fileName).c_str(), + (dest_path / f.fileName).c_str(), overwrite)) + throw_testWinFuncFailed("CopyFile", ((src_path / f.fileName).string() + " => " + + (dest_path / f.fileName).string())); + } + for (auto r : recurse) + recursive_copy_files(src_path / r, dest_path / r, overwrite); +} + +ScopedLoadLibrary::ScopedLoadLibrary(const wchar_t* dll_path) + : m_mod(LoadLibrary(dll_path)) +{} +ScopedLoadLibrary::~ScopedLoadLibrary() +{ + if (m_mod) + FreeLibrary(m_mod); +} + +}; // namespace test diff --git a/libs/usvfs/test/test_utils/test_helpers.h b/libs/usvfs/test/test_utils/test_helpers.h new file mode 100644 index 0000000..7ea8950 --- /dev/null +++ b/libs/usvfs/test/test_utils/test_helpers.h @@ -0,0 +1,165 @@ +#pragma once + +#include "windows_sane.h" + +#include <filesystem> +#include <format> +#include <memory> + +namespace test +{ + +class FuncFailed : public std::runtime_error +{ +public: + FuncFailed(const char* func) : std::runtime_error(msg(func)) {} + FuncFailed(const char* func, unsigned long res) + : std::runtime_error(msg(func, nullptr, &res)) + {} + FuncFailed(const char* func, const char* arg1) : std::runtime_error(msg(func, arg1)) + {} + FuncFailed(const char* func, const char* arg1, unsigned long res) + : std::runtime_error(msg(func, arg1, &res)) + {} + FuncFailed(const char* func, const char* what, const char* arg1) + : std::runtime_error(msg(func, arg1, nullptr, what)) + {} + FuncFailed(const char* func, const char* what, const char* arg1, unsigned long res) + : std::runtime_error(msg(func, arg1, &res, what)) + {} + +private: + std::string msg(std::string_view func, const char* arg1 = nullptr, + const unsigned long* res = nullptr, const char* what = nullptr); +}; + +class WinFuncFailed : public std::runtime_error +{ +public: + using runtime_error::runtime_error; +}; + +class WinFuncFailedGenerator +{ +public: + WinFuncFailedGenerator(DWORD gle = GetLastError()) : m_gle(gle) {} + WinFuncFailedGenerator(const WinFuncFailedGenerator&) = delete; + + DWORD lastError() const { return m_gle; } + + WinFuncFailed operator()(std::basic_string_view<char> func) + { + return WinFuncFailed(std::format("{} failed : lastError={}", func, m_gle)); + } + + WinFuncFailed operator()(std::basic_string_view<char> func, unsigned long res) + { + return WinFuncFailed( + std::format("{} failed : result=({:#x}), lastError={}", func, res, m_gle)); + } + + WinFuncFailed operator()(std::string_view func, std::basic_string_view<char> arg1) + { + return WinFuncFailed( + std::format("{} failed : {}, lastError={}", func, arg1, m_gle)); + } + + WinFuncFailed operator()(std::string_view func, std::basic_string_view<char> arg1, + unsigned long res) + { + return WinFuncFailed(std::format("{} failed : {}, result=({:#x}), lastError={}", + func, arg1, res, m_gle)); + } + +private: + DWORD m_gle; +}; + +// trick to guarantee the evalutation of GetLastError() before the evalution of the +// parameters to the WinFuncFailed message generation +template <class... Args> +[[noreturn]] void throw_testWinFuncFailed(std::string_view func, Args&&... args) +{ + ::test::WinFuncFailedGenerator exceptionGenerator; + throw exceptionGenerator(func, std::forward<Args>(args)...); +} + +class ScopedFILE +{ +public: + // try to open the given filepath with the given mode, if it fails, set err to + // the return code of _wfopen_s and return a nulled scoped file + // + static ScopedFILE open(const std::filesystem::path& filepath, std::wstring_view mode, + errno_t& err); + + // same as above but throw a WinFuncFailed() exception if opening the file failed + // + static ScopedFILE open(const std::filesystem::path& filepath, std::wstring_view mode); + +public: + ScopedFILE(FILE* f = nullptr) : m_f(f, &fclose) {} + + ScopedFILE(ScopedFILE&& other) noexcept = default; + ~ScopedFILE() = default; + + ScopedFILE(const ScopedFILE&) = delete; + + void close() { m_f = nullptr; } + + operator bool() const { return static_cast<bool>(m_f); } + operator FILE*() const { return m_f.get(); } + +private: + std::unique_ptr<FILE, decltype(&fclose)> m_f; +}; + +using std::filesystem::path; + +// path functions assume they are called by a test executable +// (calculate the requested path relative to the current executable path) + +path path_of_test_bin(const path& relative = path()); +path path_of_test_temp(const path& relative = path()); +path path_of_test_fixtures(const path& relative = path()); +path path_of_usvfs_lib(const path& relative = path()); + +std::string platform_dependant_executable(const char* name, const char* ext = "exe", + const char* platform = nullptr); + +// if full_path is a subfolder of base returns only the relative path, +// if full_path and base are the same folder "." is returned, +// otherwise full_path is returned unchanged +path path_as_relative(const path& base, const path& full_path); + +std::vector<char> read_small_file(const path& file, bool binary = true); + +// true iff the the contents of the two files is exactly the same +bool compare_files(const path& file1, const path& file2, bool binary = true); + +// return true iff the given path is an empty (optionally true also if path doesn't +// exist) +bool is_empty_folder(const path& dpath, bool or_doesnt_exist = false); + +void delete_file(const path& file); + +// Recursively deletes the given path and all the files and directories under it +// Use with care!!! +void recursive_delete_files(const path& dpath); + +// Recursively copies all files and directories from srcPath to destPath +void recursive_copy_files(const path& src_path, const path& dest_path, bool overwrite); + +class ScopedLoadLibrary +{ +public: + ScopedLoadLibrary(const wchar_t* dll_path); + ~ScopedLoadLibrary(); + + // returns zero if load library failed + operator HMODULE() const { return m_mod; } + +private: + HMODULE m_mod; +}; +}; // namespace test diff --git a/libs/usvfs/test/thooklib_test/CMakeLists.txt b/libs/usvfs/test/thooklib_test/CMakeLists.txt new file mode 100644 index 0000000..cca9e10 --- /dev/null +++ b/libs/usvfs/test/thooklib_test/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(GTest CONFIG REQUIRED) +find_package(Boost CONFIG REQUIRED COMPONENTS thread) + +# not sure why there is a test_hooks.cpp here? +add_executable(thooklib_test main.cpp) +usvfs_set_test_properties(thooklib_test) +target_link_libraries(thooklib_test + PRIVATE test_utils shared thooklib Boost::thread + GTest::gtest GTest::gtest_main) diff --git a/libs/usvfs/test/thooklib_test/main.cpp b/libs/usvfs/test/thooklib_test/main.cpp new file mode 100644 index 0000000..df48198 --- /dev/null +++ b/libs/usvfs/test/thooklib_test/main.cpp @@ -0,0 +1,235 @@ +#include <gtest/gtest.h> +#include <hooklib.h> +#include <iostream> +#include <ttrampolinepool.h> +#include <utility.h> +#include <windows_sane.h> +// #include <boost/thread.hpp> +#include <boost/filesystem.hpp> +#include <exceptionex.h> +#include <spdlog/sinks/stdout_sinks.h> +#include <spdlog/spdlog.h> +#include <winapi.h> + +namespace fs = boost::filesystem; + +#include <stringutils.h> + +using namespace std; +using namespace HookLib; + +class TempFile +{ +public: + TempFile(const wchar_t* relative) : wpath(winapi::wide::getModuleFileName(nullptr)) + { + size_t path_end = wpath.rfind(L'\\'); + if (path_end != std::wstring::npos) { + wpath.erase(path_end + 1); + wpath += L"..\\temp\\"; + wpath += relative; + } else + wpath = relative; + path = + usvfs::shared::string_cast<std::string>(wpath, usvfs::shared::CodePage::UTF8); + } + + const char* c_str() const { return path.c_str(); } + const wchar_t* w_str() const { return wpath.c_str(); } + +private: + std::string path; + std::wstring wpath; +}; + +static const HANDLE MARKERHANDLE = reinterpret_cast<HANDLE>(0x1CC0FFEE); +static const TempFile VALID_FILENAME{L"VALID_FILENAME"}; +static const TempFile INVALID_FILENAME{L"\\<>/"}; + +#include "test_hooks.cpp" + +static bool stubCalled = false; + +void __cdecl CreateFileStub(LPVOID) +{ + stubCalled = true; +} + +class HookingTest : public testing::Test +{ +public: + void SetUp() + { + /* typedef sinks::synchronous_sink<sinks::text_ostream_backend> text_sink; + boost::shared_ptr<text_sink> sink = boost::make_shared<text_sink>(); + + // Add a stream to write log to + sink->locked_backend()->add_stream(boost::make_shared<std::ofstream>("c:\\temp\\testing_out.log")); + + // Register the sink in the logging core + logging::core::get()->add_sink(sink); + + sink->set_filter(expr::attr<LogLevel>("Severity") >= LogLevel::Debug);*/ + } + + void TearDown() {} + +private: +}; + +TEST(GetProcAddressTest, ReturnsValidResults) +{ + HMODULE mh = GetModuleHandleA("KernelBase.dll"); + EXPECT_NE(nullptr, mh); + EXPECT_EQ(GetProcAddress(mh, "CreateFileA"), MyGetProcAddress(mh, "CreateFileA")); +} + +TEST_F(HookingTest, CanHook) +{ + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + HOOKHANDLE hook = InstallHook(k32Mod, "CreateFileW", THCreateFileW_1); + if (hook == INVALID_HOOK) { + k32Mod = GetModuleHandleA("kernelbase.dll"); + hook = InstallHook(k32Mod, "CreateFileW", THCreateFileW_1); + } + EXPECT_NE(INVALID_HOOK, hook); + RemoveHook(hook); +} + +TEST_F(HookingTest, CanStub) +{ + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + HOOKHANDLE hook = InstallStub(k32Mod, "CreateFileW", CreateFileStub); + if (hook == INVALID_HOOK) { + k32Mod = GetModuleHandleA("kernelbase.dll"); + hook = InstallStub(k32Mod, "CreateFileW", CreateFileStub); + } + EXPECT_NE(INVALID_HOOK, hook); + RemoveHook(hook); +} + +TEST_F(HookingTest, RemoveHook) +{ + // test that we can remove a hook + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + HOOKHANDLE hook = InstallHook(k32Mod, "CreateFileA", THCreateFileA_1); + if (hook == INVALID_HOOK) { + k32Mod = GetModuleHandleA("kernelbase.dll"); + hook = InstallHook(k32Mod, "CreateFileA", THCreateFileA_1); + } + + EXPECT_NE(INVALID_HOOK, hook); + RemoveHook(hook); + HANDLE test = CreateFileA(INVALID_FILENAME.c_str(), GENERIC_READ, 0, nullptr, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + EXPECT_EQ(INVALID_HANDLE_VALUE, test); +} + +TEST_F(HookingTest, CreateFileStubTest) +{ + stubCalled = false; + // test if our stub works + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + HOOKHANDLE hook = InstallStub(k32Mod, "CreateFileA", CreateFileStub); + if (hook == INVALID_HOOK) { + k32Mod = GetModuleHandleA("kernelbase.dll"); + hook = InstallStub(k32Mod, "CreateFileA", CreateFileStub); + } + EXPECT_NE(INVALID_HOOK, hook); + HANDLE test = CreateFileA(INVALID_FILENAME.c_str(), GENERIC_READ, 0, nullptr, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + RemoveHook(hook); + EXPECT_EQ(true, stubCalled); + EXPECT_EQ(INVALID_HANDLE_VALUE, test); +} + +TEST_F(HookingTest, CreateFileHook) +{ + // test if our hook works + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + HOOKHANDLE hook = InstallHook(k32Mod, "CreateFileA", THCreateFileA_1); + if (hook == INVALID_HOOK) { + k32Mod = GetModuleHandleA("kernelbase.dll"); + hook = InstallHook(k32Mod, "CreateFileA", THCreateFileA_1); + } + HANDLE test = CreateFileA(INVALID_FILENAME.c_str(), GENERIC_READ, 0, nullptr, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + RemoveHook(hook); + EXPECT_EQ(MARKERHANDLE, test); +} + +TEST_F(HookingTest, CreateFileWHook) +{ + // test if our hook works + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + HOOKHANDLE hook = InstallHook(k32Mod, "CreateFileW", THCreateFileW_1); + if (hook == INVALID_HOOK) { + k32Mod = GetModuleHandleA("kernelbase.dll"); + hook = InstallHook(k32Mod, "CreateFileW", THCreateFileW_1); + } + HANDLE test = CreateFileW(INVALID_FILENAME.w_str(), 0x42, 0x43, + (LPSECURITY_ATTRIBUTES)0x44, 0x45, 0x46, (HANDLE)0x47); + RemoveHook(hook); + EXPECT_EQ(MARKERHANDLE, test); +} + +TEST_F(HookingTest, CreateFileHookRecursion) +{ + // test that the trampoline works, so we can call the original function from + // within the hook + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + HOOKHANDLE hook = InstallHook(k32Mod, "CreateFileA", THCreateFileA_1); + if (hook == INVALID_HOOK) { + k32Mod = GetModuleHandleA("kernelbase.dll"); + hook = InstallHook(k32Mod, "CreateFileA", THCreateFileA_1); + } + HANDLE test = CreateFileA(VALID_FILENAME.c_str(), GENERIC_READ, 0, nullptr, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + RemoveHook(hook); + EXPECT_NE(MARKERHANDLE, test); +} + +TEST_F(HookingTest, Threading) +{ + // test that multiple threads can concurrently call a hooked function without + // incorrect results. + // TODO: this test doesn't reliably find thread-unsafeties + // NOTE: the hooklib currently does not claim that hook installation or removal + // is thread-safe, only the hooked functions shouldn't become less thread-safe by + // being hooked! + + static const int NUM_THREADS = 100; + static const int NUM_TRIES = 1000; + + HMODULE k32Mod = GetModuleHandleA("kernel32.dll"); + HOOKHANDLE hook = InstallHook(k32Mod, "CreateFileA", THCreateFileA_1); + if (hook == INVALID_HOOK) { + k32Mod = GetModuleHandleA("kernelbase.dll"); + hook = InstallHook(k32Mod, "CreateFileA", THCreateFileA_1); + } + std::thread threads[NUM_THREADS]; + for (int i = 0; i < NUM_THREADS; ++i) { + threads[i] = std::thread([i] { + for (int count = 0; count < NUM_TRIES; ++count) { + HANDLE test = CreateFileA(INVALID_FILENAME.c_str(), GENERIC_READ, 0, nullptr, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); + EXPECT_EQ(MARKERHANDLE, test); + } + }); + } + + for (int i = 0; i < NUM_THREADS; ++i) { + threads[i].join(); + } + + RemoveHook(hook); +} + +int main(int argc, char** argv) +{ + auto logger = spdlog::stdout_logger_mt("usvfs"); + logger->set_level(spdlog::level::warn); + TrampolinePool::initialize(); + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/libs/usvfs/test/thooklib_test/test_hooks.cpp b/libs/usvfs/test/thooklib_test/test_hooks.cpp new file mode 100644 index 0000000..edde2c8 --- /dev/null +++ b/libs/usvfs/test/thooklib_test/test_hooks.cpp @@ -0,0 +1,38 @@ +#include <Windows.h> +#include <gtest/gtest.h> + +HANDLE WINAPI THCreateFileA_1(LPCSTR lpFileName, DWORD dwDesiredAccess, + DWORD dwShareMode, + LPSECURITY_ATTRIBUTES lpSecurityAttributes, + DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, + HANDLE hTemplateFile) +{ + if (strcmp(lpFileName, INVALID_FILENAME.c_str()) == 0) { + return MARKERHANDLE; + } else { + HANDLE res = + ::CreateFileA(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, + dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); + return res; + } +} + +HANDLE WINAPI THCreateFileW_1(LPCWSTR lpFileName, DWORD dwDesiredAccess, + DWORD dwShareMode, + LPSECURITY_ATTRIBUTES lpSecurityAttributes, + DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, + HANDLE hTemplateFile) +{ + if (wcscmp(lpFileName, INVALID_FILENAME.w_str()) == 0) { + EXPECT_EQ(0x42, dwDesiredAccess); + EXPECT_EQ(0x43, dwShareMode); + EXPECT_EQ(0x44, (int)lpSecurityAttributes); + EXPECT_EQ(0x45, dwCreationDisposition); + EXPECT_EQ(0x46, dwFlagsAndAttributes); + EXPECT_EQ(0x47, (int)hTemplateFile); + return MARKERHANDLE; + } else { + return ::CreateFileW(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, + dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); + } +} diff --git a/libs/usvfs/test/tinjectlib_test/CMakeLists.txt b/libs/usvfs/test/tinjectlib_test/CMakeLists.txt new file mode 100644 index 0000000..9bd3f7b --- /dev/null +++ b/libs/usvfs/test/tinjectlib_test/CMakeLists.txt @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(GTest CONFIG REQUIRED) + +# binary and binary injected +add_executable(testinject_bin testinject_bin/main.cpp) +usvfs_set_test_properties(testinject_bin FOLDER tinjectlib) + +add_library(testinject_dll SHARED testinject_dll/main.cpp testinject_dll/main.h) +usvfs_set_test_properties(testinject_dll FOLDER tinjectlib) + +# actual test executable +add_executable(tinjectlib_test main.cpp) +usvfs_set_test_properties(tinjectlib_test FOLDER tinjectlib) +target_link_libraries(tinjectlib_test PRIVATE tinjectlib shared GTest::gtest GTest::gtest_main) +add_dependencies(tinjectlib_test testinject_bin testinject_dll) diff --git a/libs/usvfs/test/tinjectlib_test/main.cpp b/libs/usvfs/test/tinjectlib_test/main.cpp new file mode 100644 index 0000000..ce48f83 --- /dev/null +++ b/libs/usvfs/test/tinjectlib_test/main.cpp @@ -0,0 +1,158 @@ +#include <boost/filesystem.hpp> +#include <boost/type_traits.hpp> +#include <gtest/gtest.h> +#include <injectlib.h> +#include <spdlog/sinks/stdout_sinks.h> +#include <spdlog/spdlog.h> +#include <winapi.h> + +using namespace usvfs::shared; +using namespace InjectLib; + +#if BOOST_ARCH_X86_64 +static const wchar_t INJECT_BIN[] = L"testinject_bin_x64.exe"; +#else +static const wchar_t INJECT_BIN[] = L"testinject_bin_x86.exe"; +#endif + +#if BOOST_ARCH_X86_64 +static const wchar_t INJECT_LIB[] = L"testinject_dll_x64.dll"; +#else +static const wchar_t INJECT_LIB[] = L"testinject_dll_x86.dll"; +#endif + +static std::shared_ptr<spdlog::logger> logger() +{ + std::shared_ptr<spdlog::logger> result = spdlog::get("test"); + if (result.get() == nullptr) { + result = spdlog::stdout_logger_mt("test"); + } + return result; +} + +bool spawn(HANDLE& processHandle, HANDLE& threadHandle) +{ + STARTUPINFO si; + ::ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + + PROCESS_INFORMATION pi; + BOOL success = ::CreateProcess(INJECT_BIN, nullptr, nullptr, nullptr, FALSE, + CREATE_SUSPENDED, nullptr, nullptr, &si, &pi); + + if (!success) { + throw windows_error("failed to start process"); + } + + processHandle = pi.hProcess; + threadHandle = pi.hThread; + + return true; +} + +TEST(InjectingTest, InjectionNoInit) +{ + // Verify lib can inject without a init function + + HANDLE process, thread; + spawn(process, thread); + EXPECT_NO_THROW(InjectLib::InjectDLL(process, thread, INJECT_LIB)); + ResumeThread(thread); + + DWORD res = WaitForSingleObject(process, INFINITE); + DWORD exitCode = NO_ERROR; + res = GetExitCodeProcess(process, &exitCode); + EXPECT_EQ(NOERROR, exitCode); + + CloseHandle(process); + CloseHandle(thread); +} + +TEST(InjectingTest, InjectionSimpleInit) +{ + // Verify lib can inject with a init function with null parameters + + HANDLE process, thread; + spawn(process, thread); + EXPECT_NO_THROW(InjectLib::InjectDLL(process, thread, INJECT_LIB, "InitNoParam")); + ResumeThread(thread); + + DWORD res = WaitForSingleObject(process, INFINITE); + DWORD exitCode = NO_ERROR; + res = GetExitCodeProcess(process, &exitCode); + EXPECT_EQ(10001, exitCode); // used init function exits process with this exit code + + CloseHandle(process); + CloseHandle(thread); +} + +TEST(InjectingTest, InjectionComplexInit) +{ + // Verify lib can inject with a init function with null parameters + + static const WCHAR param[] = L"magic_parameter"; + HANDLE process, thread; + spawn(process, thread); + EXPECT_NO_THROW(InjectLib::InjectDLL(process, thread, INJECT_LIB, "InitComplexParam", + reinterpret_cast<LPCVOID>(param), + wcslen(param) * sizeof(WCHAR))); + + ResumeThread(thread); + + DWORD res = WaitForSingleObject(process, INFINITE); + DWORD exitCode = NO_ERROR; + res = GetExitCodeProcess(process, &exitCode); + EXPECT_EQ(10002, exitCode); // used init function exits process with this exit code + + CloseHandle(process); + CloseHandle(thread); +} + +TEST(InjectingTest, InjectionNoQuitInit) +{ + // Verify lib can inject with a init function with null parameters + + HANDLE process, thread; + spawn(process, thread); + EXPECT_NO_THROW(InjectLib::InjectDLL(process, thread, INJECT_LIB, "InitNoQuit")); + ResumeThread(thread); + + DWORD res = WaitForSingleObject(process, INFINITE); + DWORD exitCode = NO_ERROR; + res = GetExitCodeProcess(process, &exitCode); + EXPECT_EQ(0, exitCode); // expect regular exit from process + + CloseHandle(process); + CloseHandle(thread); +} + +TEST(InjectingTest, InjectionSkipInit) +{ + // verify the skip-on-missing mechanism for init function works + + HANDLE process, thread; + spawn(process, thread); + EXPECT_NO_THROW(InjectLib::InjectDLL(process, thread, INJECT_LIB, "__InitInvalid", + nullptr, 0, true)); + ResumeThread(thread); + + DWORD res = WaitForSingleObject(process, INFINITE); + DWORD exitCode = NO_ERROR; + res = GetExitCodeProcess(process, &exitCode); + EXPECT_EQ(NOERROR, exitCode); + + CloseHandle(process); + CloseHandle(thread); +} + +int main(int argc, char** argv) +{ + auto logger = spdlog::stdout_logger_mt("usvfs"); + logger->set_level(spdlog::level::warn); + + boost::filesystem::path filePath(winapi::wide::getModuleFileName(nullptr)); + SetCurrentDirectoryW(filePath.parent_path().wstring().c_str()); + + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/libs/usvfs/test/tinjectlib_test/testinject_bin/main.cpp b/libs/usvfs/test/tinjectlib_test/testinject_bin/main.cpp new file mode 100644 index 0000000..f8b643a --- /dev/null +++ b/libs/usvfs/test/tinjectlib_test/testinject_bin/main.cpp @@ -0,0 +1,4 @@ +int main() +{ + return 0; +} diff --git a/libs/usvfs/test/tinjectlib_test/testinject_dll/main.cpp b/libs/usvfs/test/tinjectlib_test/testinject_dll/main.cpp new file mode 100644 index 0000000..8339e97 --- /dev/null +++ b/libs/usvfs/test/tinjectlib_test/testinject_dll/main.cpp @@ -0,0 +1,36 @@ +#include "main.h" + +void __cdecl InitNoQuit(LPVOID, size_t) +{ + // nop +} + +void __cdecl InitNoParam(LPVOID, size_t) +{ + ExitProcess(10001); +} + +void __cdecl InitComplexParam(LPVOID userData, size_t) +{ + LPCWSTR string = (LPCWSTR)userData; + if (wcscmp(string, L"magic_parameter") == 0) { + ExitProcess(10002); + } else { + ExitProcess(20003); + } +} + +BOOL APIENTRY DllMain(HMODULE, DWORD reasonForCall, LPVOID) +{ + switch (reasonForCall) { + case DLL_PROCESS_ATTACH: { + } break; + case DLL_PROCESS_DETACH: { + } break; + case DLL_THREAD_ATTACH: { + } break; + case DLL_THREAD_DETACH: { + } break; + } + return TRUE; +} diff --git a/libs/usvfs/test/tinjectlib_test/testinject_dll/main.h b/libs/usvfs/test/tinjectlib_test/testinject_dll/main.h new file mode 100644 index 0000000..6faf800 --- /dev/null +++ b/libs/usvfs/test/tinjectlib_test/testinject_dll/main.h @@ -0,0 +1,11 @@ +#pragma once + +#define WIN32_LEAN_AND_MEAN +#include <windows.h> + +extern "C" + __declspec(dllexport) void __cdecl InitNoQuit(LPVOID userData, size_t userDataSize); +extern "C" __declspec(dllexport) void __cdecl InitNoParam(LPVOID userData, + size_t userDataSize); +extern "C" __declspec(dllexport) void __cdecl InitComplexParam(LPVOID userData, + size_t userDataSize); diff --git a/libs/usvfs/test/tvfs_test/CMakeLists.txt b/libs/usvfs/test/tvfs_test/CMakeLists.txt new file mode 100644 index 0000000..f66a7b4 --- /dev/null +++ b/libs/usvfs/test/tvfs_test/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(GTest CONFIG REQUIRED) + +add_executable(tvfs_test main.cpp) +usvfs_set_test_properties(tvfs_test) +target_link_libraries(tvfs_test PRIVATE test_utils usvfs_helper GTest::gtest GTest::gmock GTest::gtest_main) +usvfs_target_link_usvfs(tvfs_test) + +# tvfs_test uses a private USVFS header so we need to include it manually +get_target_property(USVFS_SOURCE_DIR usvfs_dll SOURCE_DIR) +target_include_directories(tvfs_test PRIVATE ${USVFS_SOURCE_DIR}) diff --git a/libs/usvfs/test/tvfs_test/main.cpp b/libs/usvfs/test/tvfs_test/main.cpp new file mode 100644 index 0000000..7c7998b --- /dev/null +++ b/libs/usvfs/test/tvfs_test/main.cpp @@ -0,0 +1,585 @@ +/* +Userspace Virtual Filesystem + +Copyright (C) 2015 Sebastian Herbord. All rights reserved. + +This file is part of usvfs. + +usvfs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +usvfs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with usvfs. If not, see <http://www.gnu.org/licenses/>. +*/ + +// this file depends on so many stuff that the easiest way is to include +// pch.h from shared +#include "pch.h" + +#include <test_helpers.h> + +#include <fstream> +#include <iostream> + +#include <inject.h> +#include <stringutils.h> +#include <windows_sane.h> + +#include <spdlog/sinks/stdout_sinks.h> +#include <spdlog/spdlog.h> + +#include <hookcontext.h> +#include <hooks/kernel32.h> +#include <hooks/ntdll.h> +#include <logging.h> +#include <stringcast.h> +#include <unicodestring.h> +#include <usvfs.h> + +#include <gmock/gmock-matchers.h> +#include <gtest/gtest.h> + +namespace spd = spdlog; + +namespace ush = usvfs::shared; + +// name of a file to be created in the virtual fs. Shouldn't exist on disc but the +// directory must exist +static LPCSTR VIRTUAL_FILEA = "C:/np.exe"; +static LPCWSTR VIRTUAL_FILEW = L"C:/np.exe"; + +// a real file on disc that has to exist +static LPCSTR REAL_FILEA = "C:/windows/notepad.exe"; +static LPCWSTR REAL_FILEW = L"C:/windows/notepad.exe"; + +static LPCSTR REAL_DIRA = "C:/windows/Logs"; +static LPCWSTR REAL_DIRW = L"C:/windows/Logs"; + +static std::shared_ptr<spdlog::logger> logger() +{ + std::shared_ptr<spdlog::logger> result = spdlog::get("test"); + if (result.get() == nullptr) { + result = spdlog::stdout_logger_mt("test"); + } + return result; +} + +auto defaultUsvfsParams(const char* instanceName = "usvfs_test") +{ + std::unique_ptr<usvfsParameters, decltype(&usvfsFreeParameters)> parameters{ + usvfsCreateParameters(), &usvfsFreeParameters}; + + usvfsSetInstanceName(parameters.get(), instanceName); + usvfsSetDebugMode(parameters.get(), true); + usvfsSetLogLevel(parameters.get(), LogLevel::Debug); + usvfsSetCrashDumpType(parameters.get(), CrashDumpsType::None); + usvfsSetCrashDumpPath(parameters.get(), ""); + + return std::move(parameters); +} + +class USVFSTest : public testing::Test +{ +public: + void SetUp() + { + SHMLogger::create("usvfs"); + // need to initialize logging in the context of the dll + usvfsInitLogging(); + } + + void TearDown() + { + std::array<char, 1024> buffer; + while (SHMLogger::instance().tryGet(buffer.data(), buffer.size())) { + std::cout << buffer.data() << std::endl; + } + SHMLogger::free(); + } + +private: +}; + +class USVFSTestWithReroute : public testing::Test +{ +public: + void SetUp() + { + SHMLogger::create("usvfs"); + // need to initialize logging in the context of the dll + usvfsInitLogging(); + + auto params = defaultUsvfsParams(); + m_Context.reset(usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); + usvfs::RedirectionTreeContainer& tree = m_Context->redirectionTable(); + tree.addFile( + ush::string_cast<std::string>(VIRTUAL_FILEW, ush::CodePage::UTF8).c_str(), + usvfs::RedirectionDataLocal(REAL_FILEA)); + } + + void TearDown() + { + std::array<char, 1024> buffer; + while (SHMLogger::instance().tryGet(buffer.data(), buffer.size())) { + std::cout << buffer.data() << std::endl; + } + m_Context.reset(); + SHMLogger::free(); + } + +private: + std::unique_ptr<usvfs::HookContext> m_Context; +}; + +class USVFSTestAuto : public testing::Test +{ +public: + void SetUp() + { + auto params = defaultUsvfsParams(); + usvfsConnectVFS(params.get()); + SHMLogger::create("usvfs"); + } + + void TearDown() + { + usvfsDisconnectVFS(); + + std::array<char, 1024> buffer; + while (SHMLogger::instance().tryGet(buffer.data(), buffer.size())) { + std::cout << buffer.data() << std::endl; + } + SHMLogger::free(); + } + +private: +}; + +TEST_F(USVFSTest, CanResizeRedirectiontree) +{ + using usvfs::shared::MissingThrow; + ASSERT_NO_THROW({ + usvfs::RedirectionTreeContainer container("treetest_shm", 1024); + for (char i = 'a'; i <= 'z'; ++i) { + for (char j = 'a'; j <= 'z'; ++j) { + std::string name = std::string(R"(C:\temp\)") + i + j; + container.addFile(name, usvfs::RedirectionDataLocal("gaga"), false); + } + } + + ASSERT_EQ("gaga", container->node("C:") + ->node("temp") + ->node("aa", MissingThrow) + ->data() + .linkTarget); + ASSERT_EQ("gaga", container->node("C:") + ->node("temp") + ->node("az", MissingThrow) + ->data() + .linkTarget); + }); +} + +/* +TEST_F(USVFSTest, CreateFileHookReportsCorrectErrorOnMissingFile) +{ + ASSERT_NO_THROW({ + USVFSParameters params; + USVFSInitParameters(¶ms, "usvfs_test", true, LogLevel::Debug, +CrashDumpsType::None, ""); std::unique_ptr<usvfs::HookContext> +ctx(CreateHookContext(params, ::GetModuleHandle(nullptr))); HANDLE res = +usvfs::hook_CreateFileW(VIRTUAL_FILEW , GENERIC_READ , FILE_SHARE_READ | +FILE_SHARE_WRITE , nullptr , OPEN_EXISTING , FILE_ATTRIBUTE_NORMAL , nullptr); + + ASSERT_EQ(INVALID_HANDLE_VALUE, res); + ASSERT_EQ(ERROR_FILE_NOT_FOUND, ::GetLastError()); + }); +} +*/ + +/* +TEST_F(USVFSTestWithReroute, CreateFileHookRedirectsFile) +{ + ASSERT_NE(INVALID_HANDLE_VALUE + , usvfs::hook_CreateFileW(VIRTUAL_FILEW + , GENERIC_READ + , FILE_SHARE_READ | FILE_SHARE_WRITE + , nullptr + , OPEN_EXISTING + , FILE_ATTRIBUTE_NORMAL + , nullptr)); +} +*/ + +TEST_F(USVFSTest, GetFileAttributesHookReportsCorrectErrorOnMissingFile) +{ + ASSERT_NO_THROW({ + try { + auto params = defaultUsvfsParams(); + std::unique_ptr<usvfs::HookContext> ctx( + usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); + DWORD res = usvfs::hook_GetFileAttributesW(VIRTUAL_FILEW); + + ASSERT_EQ(INVALID_FILE_ATTRIBUTES, res); + ASSERT_EQ(ERROR_FILE_NOT_FOUND, ::GetLastError()); + } catch (const std::exception& e) { + logger()->error("Exception: {}", e.what()); + throw; + } + }); +} + +TEST_F(USVFSTest, GetFileAttributesHookRedirectsFile) +{ + auto params = defaultUsvfsParams(); + std::unique_ptr<usvfs::HookContext> ctx( + usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); + usvfs::RedirectionTreeContainer& tree = ctx->redirectionTable(); + + tree.addFile( + ush::string_cast<std::string>(VIRTUAL_FILEW, ush::CodePage::UTF8).c_str(), + usvfs::RedirectionDataLocal(REAL_FILEA)); + + ASSERT_EQ(::GetFileAttributesW(REAL_FILEW), + usvfs::hook_GetFileAttributesW(VIRTUAL_FILEW)); +} +/* +TEST_F(USVFSTest, GetFullPathNameOnRegularCurrentDirectory) +{ + USVFSParameters params; + USVFSInitParameters(¶ms, "usvfs_test", true, LogLevel::Debug, +CrashDumpsType::None, ""); std::unique_ptr<usvfs::HookContext> +ctx(CreateHookContext(params, ::GetModuleHandle(nullptr))); + + std::wstring expected = winapi::wide::getCurrentDirectory() + L"\\filename.txt"; + + DWORD bufferLength = 32767; + std::unique_ptr<wchar_t[]> buffer(new wchar_t[bufferLength]); + LPWSTR filePart = nullptr; + + DWORD res = usvfs::hook_GetFullPathNameW(L"filename.txt", bufferLength, buffer.get(), +&filePart); + + ASSERT_NE(0UL, res); + ASSERT_EQ(expected, std::wstring(buffer.get())); +}*/ + +// small wrapper to call usvfs::hook_NtOpenFile with a path +// +// at some point in time, changes were made to USVFS such that calling a hooked +// function from a handle obtained from a non-hooked function would not work anymore, +// meaning that function such as CreateFileW that have no hook equivalent cannot +// be used to test hook functions +// +// this function is useful to simulate a CreateFileW by internally using the hook +// version of NtOpenFile +// +HANDLE hooked_NtOpenFile(LPCWSTR path, ACCESS_MASK accessMask, ULONG shareAccess, + ULONG openOptions) +{ + constexpr size_t BUFFER_SIZE = 2048; + IO_STATUS_BLOCK statusBlock; + OBJECT_ATTRIBUTES attributes; + attributes.SecurityDescriptor = 0; + attributes.SecurityQualityOfService = 0; + attributes.RootDirectory = 0; + attributes.Attributes = 0; + attributes.Length = sizeof(OBJECT_ATTRIBUTES); + + WCHAR stringBuffer[BUFFER_SIZE]; + UNICODE_STRING string; + string.Buffer = stringBuffer; + lstrcpyW(stringBuffer, L"\\??\\"); + lstrcatW(stringBuffer, path); + string.Length = static_cast<USHORT>(lstrlenW(stringBuffer) * 2); + string.MaximumLength = BUFFER_SIZE; + attributes.ObjectName = &string; + + HANDLE ret = INVALID_HANDLE_VALUE; + if (usvfs::hook_NtOpenFile(&ret, accessMask, &attributes, &statusBlock, shareAccess, + openOptions) != STATUS_SUCCESS) { + return INVALID_HANDLE_VALUE; + } + + return ret; +} + +TEST_F(USVFSTest, NtQueryDirectoryFileRegularFile) +{ + auto params = defaultUsvfsParams(); + std::unique_ptr<usvfs::HookContext> ctx( + usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); + + HANDLE hdl = + hooked_NtOpenFile(L"C:\\", FILE_GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT); + ASSERT_NE(INVALID_HANDLE_VALUE, hdl); + + IO_STATUS_BLOCK status; + char buffer[1024]; + + usvfs::hook_NtQueryDirectoryFile(hdl, nullptr, nullptr, nullptr, &status, buffer, + 1024, FileDirectoryInformation, TRUE, nullptr, TRUE); + + ASSERT_EQ(STATUS_SUCCESS, status.Status); + + usvfs::hook_NtClose(hdl); +} + +TEST_F(USVFSTest, NtQueryDirectoryFileFindsVirtualFile) +{ + auto params = defaultUsvfsParams(); + std::unique_ptr<usvfs::HookContext> ctx( + usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); + usvfs::RedirectionTreeContainer& tree = ctx->redirectionTable(); + + tree.addFile(L"C:\\np.exe", usvfs::RedirectionDataLocal(REAL_FILEA)); + + HANDLE hdl = + hooked_NtOpenFile(L"C:\\", FILE_GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT); + ASSERT_NE(INVALID_HANDLE_VALUE, hdl); + + IO_STATUS_BLOCK status; + char buffer[1024]; + + usvfs::UnicodeString fileName(L"np.exe"); + + usvfs::hook_NtQueryDirectoryFile(hdl, nullptr, nullptr, nullptr, &status, buffer, + 1024, FileDirectoryInformation, TRUE, + static_cast<PUNICODE_STRING>(fileName), TRUE); + + FILE_DIRECTORY_INFORMATION* info = + reinterpret_cast<FILE_DIRECTORY_INFORMATION*>(buffer); + ASSERT_EQ(STATUS_SUCCESS, status.Status); + ASSERT_EQ(0, wcscmp(info->FileName, L"np.exe")); + + usvfs::hook_NtClose(hdl); +} + +TEST_F(USVFSTest, NtQueryDirectoryFileExVirtualFile) +{ + auto params = defaultUsvfsParams(); + std::unique_ptr<usvfs::HookContext> ctx( + usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); + usvfs::RedirectionTreeContainer& tree = ctx->redirectionTable(); + + tree.addFile(L"C:\\0123456789.txt", usvfs::RedirectionDataLocal(REAL_FILEA)); + tree.addFile(L"C:\\123456", usvfs::RedirectionDataLocal(REAL_FILEA)); + tree.addFile(L"C:\\abcdef", usvfs::RedirectionDataLocal(REAL_FILEA)); + tree.addFile(L"C:\\abcdefghijklmnopqrstuvwxyz.txt", + usvfs::RedirectionDataLocal(REAL_FILEA)); + + HANDLE hdl = + hooked_NtOpenFile(L"C:\\", FILE_GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, + FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT); + ASSERT_NE(INVALID_HANDLE_VALUE, hdl); + + IO_STATUS_BLOCK status; + + constexpr size_t BUFFER_SIZE = 2048; + char buffer[BUFFER_SIZE]; + + std::vector<std::wstring> foundFiles; + while (usvfs::hook_NtQueryDirectoryFileEx( + hdl, nullptr, nullptr, nullptr, &status, buffer, BUFFER_SIZE, + FileFullDirectoryInformation, 0, nullptr) == STATUS_SUCCESS) { + std::size_t offset = 0; + while (offset < BUFFER_SIZE) { + const auto* info = reinterpret_cast<FILE_FULL_DIR_INFORMATION*>(buffer + offset); + foundFiles.emplace_back(info->FileName, info->FileNameLength / sizeof(wchar_t)); + + if (info->NextEntryOffset == 0) { + break; // no more entries + } + + offset += info->NextEntryOffset; + } + } + + ASSERT_THAT(foundFiles, + ::testing::IsSupersetOf({L"0123456789.txt", L"123456", L"abcdef", + L"abcdefghijklmnopqrstuvwxyz.txt"})); + + usvfs::hook_NtClose(hdl); +} + +TEST_F(USVFSTest, NtQueryObjectVirtualFile) +{ + std::wstring c_drive_device; + { + // find the device path for C: + HANDLE hdl = ::CreateFileW( + L"C:\\", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, + OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + ASSERT_NE(INVALID_HANDLE_VALUE, hdl); + + char buffer[2048]; + ASSERT_EQ(STATUS_SUCCESS, ::NtQueryObject(hdl, ObjectNameInformation, buffer, + sizeof(buffer), nullptr)); + + OBJECT_NAME_INFORMATION* information = + reinterpret_cast<OBJECT_NAME_INFORMATION*>(buffer); + + c_drive_device = + std::wstring(information->Name.Buffer, information->Name.Length / 2); + + ::CloseHandle(hdl); + } + + auto params = defaultUsvfsParams(); + std::unique_ptr<usvfs::HookContext> ctx( + usvfsCreateHookContext(*params, ::GetModuleHandle(nullptr))); + usvfs::RedirectionTreeContainer& tree = ctx->redirectionTable(); + + tree.addFile(L"C:\\np.exe", usvfs::RedirectionDataLocal(REAL_FILEA)); + + HANDLE hdl = hooked_NtOpenFile(L"C:\\np.exe", FILE_GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + FILE_NON_DIRECTORY_FILE | FILE_OPEN_FOR_BACKUP_INTENT); + ASSERT_NE(INVALID_HANDLE_VALUE, hdl) << "last error=" << ::GetLastError(); + + { + char buffer[1024]; + IO_STATUS_BLOCK status; + const auto res = usvfs::hook_NtQueryInformationFile( + hdl, &status, buffer, sizeof(buffer), FileNameInformation); + ASSERT_EQ(STATUS_SUCCESS, res); + ASSERT_EQ(STATUS_SUCCESS, status.Status); + + FILE_NAME_INFORMATION* fileNameInfo = + reinterpret_cast<FILE_NAME_INFORMATION*>(buffer); + ASSERT_EQ(L"\\np.exe", + std::wstring(fileNameInfo->FileName, fileNameInfo->FileNameLength / 2)); + } + + { + char buffer[1024]; + IO_STATUS_BLOCK status; + const auto res = usvfs::hook_NtQueryInformationFile( + hdl, &status, buffer, sizeof(buffer), FileNormalizedNameInformation); + ASSERT_EQ(STATUS_SUCCESS, res); + ASSERT_EQ(STATUS_SUCCESS, status.Status); + ASSERT_EQ(sizeof(ULONG) + 7 * 2, status.Information); + + FILE_NAME_INFORMATION* fileNameInfo = + reinterpret_cast<FILE_NAME_INFORMATION*>(buffer); + ASSERT_EQ(L"\\np.exe", + std::wstring(fileNameInfo->FileName, fileNameInfo->FileNameLength / 2)); + } + + // buffer of size should be too small for the original path (\Windows\notepad.exe) + // but not for \np.exe + { + // the required size should be sizeof(ULONG) + 7 * 2 but apparently that is + // not enough for the CI so using 16 * 2 which should be large enough for + // the hooked version, but still too short for the non-hooked one + char buffer[sizeof(ULONG) + 7 * 2]; + IO_STATUS_BLOCK status; + NTSTATUS res; + + res = ::NtQueryInformationFile(hdl, &status, buffer, sizeof(buffer), + FileNameInformation); + ASSERT_EQ(STATUS_BUFFER_OVERFLOW, res); + ASSERT_EQ(STATUS_BUFFER_OVERFLOW, status.Status); + + res = usvfs::hook_NtQueryInformationFile(hdl, &status, buffer, sizeof(buffer), + FileNameInformation); + ASSERT_EQ(STATUS_SUCCESS, res); + ASSERT_EQ(STATUS_SUCCESS, status.Status); + ASSERT_EQ(sizeof(ULONG) + 7 * 2, status.Information); + + FILE_NAME_INFORMATION* fileNameInfo = + reinterpret_cast<FILE_NAME_INFORMATION*>(buffer); + ASSERT_EQ(L"\\np.exe", + std::wstring(fileNameInfo->FileName, fileNameInfo->FileNameLength / 2)); + } + + { + char buffer[2048]; + const auto res = usvfs::hook_NtQueryObject(hdl, ObjectNameInformation, buffer, + sizeof(buffer), nullptr); + ASSERT_EQ(STATUS_SUCCESS, res); + + OBJECT_NAME_INFORMATION* information = + reinterpret_cast<OBJECT_NAME_INFORMATION*>(buffer); + ASSERT_EQ(c_drive_device + L"np.exe", + std::wstring(information->Name.Buffer, + information->Name.Length / sizeof(wchar_t))); + } + + { + // expected length is sizeof struct + size of path (in bytes), including the + // null-character + const auto expectedLength = + sizeof(OBJECT_NAME_INFORMATION) + c_drive_device.size() * 2 + 12 + 2; + ULONG requiredLength; + NTSTATUS res; + char buffer[2048]; + + res = + usvfs::hook_NtQueryObject(hdl, ObjectNameInformation, buffer, + sizeof(OBJECT_NAME_INFORMATION) - 1, &requiredLength); + ASSERT_EQ(STATUS_INFO_LENGTH_MISMATCH, res); + ASSERT_EQ(expectedLength, requiredLength); + + res = usvfs::hook_NtQueryObject(hdl, ObjectNameInformation, buffer, + sizeof(OBJECT_NAME_INFORMATION), &requiredLength); + ASSERT_EQ(STATUS_BUFFER_OVERFLOW, res); + ASSERT_EQ(expectedLength, requiredLength); + } + + usvfs::hook_NtClose(hdl); +} + +TEST_F(USVFSTestAuto, CannotCreateLinkToFileInNonexistantDirectory) +{ + ASSERT_EQ(FALSE, usvfsVirtualLinkFile( + REAL_FILEW, L"c:/this_directory_shouldnt_exist/np.exe", FALSE)); +} + +TEST_F(USVFSTestAuto, CanCreateMultipleLinks) +{ + static LPCWSTR outFile = LR"(C:\np.exe)"; + static LPCWSTR outDir = LR"(C:\logs)"; + static LPCWSTR outDirCanonizeTest = LR"(C:\.\not/../logs\.\a\.\b\.\c\..\.\..\.\..\)"; + ASSERT_EQ(TRUE, usvfsVirtualLinkFile(REAL_FILEW, outFile, 0)); + ASSERT_EQ(TRUE, usvfsVirtualLinkDirectoryStatic(REAL_DIRW, outDir, 0)); + + // both file and dir exist and have the correct type + ASSERT_NE(INVALID_FILE_ATTRIBUTES, usvfs::hook_GetFileAttributesW(outFile)); + ASSERT_NE(INVALID_FILE_ATTRIBUTES, usvfs::hook_GetFileAttributesW(outDir)); + ASSERT_EQ(0UL, usvfs::hook_GetFileAttributesW(outFile) & FILE_ATTRIBUTE_DIRECTORY); + ASSERT_NE(0UL, usvfs::hook_GetFileAttributesW(outDir) & FILE_ATTRIBUTE_DIRECTORY); + ASSERT_NE(0UL, usvfs::hook_GetFileAttributesW(outDirCanonizeTest) & + FILE_ATTRIBUTE_DIRECTORY); +} + +int main(int argc, char** argv) +{ + using namespace test; + + auto dllPath = path_of_usvfs_lib(platform_dependant_executable("usvfs", "dll")); + ScopedLoadLibrary loadDll(dllPath.c_str()); + if (!loadDll) { + std::wcerr << L"failed to load usvfs dll: " << dllPath.c_str() << L", " + << GetLastError() << std::endl; + return 1; + } + + // note: this makes the logger available only to functions statically linked to the + // test binary, not those called in the dll + auto logger = spdlog::stdout_logger_mt("usvfs"); + logger->set_level(spdlog::level::warn); + testing::InitGoogleTest(&argc, argv); + int res = RUN_ALL_TESTS(); + + return res; +} diff --git a/libs/usvfs/test/usvfs_global_test_runner/CMakeLists.txt b/libs/usvfs/test/usvfs_global_test_runner/CMakeLists.txt new file mode 100644 index 0000000..88ff0c2 --- /dev/null +++ b/libs/usvfs/test/usvfs_global_test_runner/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(GTest CONFIG REQUIRED) +find_package(Boost CONFIG REQUIRED COMPONENTS filesystem) + +# other executables +add_executable(usvfs_global_test + usvfs_global_test/usvfs_global_test.cpp +) +usvfs_set_test_properties(usvfs_global_test FOLDER usvfs_global_test) +target_link_libraries(usvfs_global_test PRIVATE gtest_utils GTest::gtest_main GTest::gmock Boost::filesystem) + +# actual test executable +add_executable(usvfs_global_test_runner + usvfs_global_test_fixture.cpp + usvfs_global_test_fixture.h + usvfs_global_test_runner.cpp +) +usvfs_set_test_properties(usvfs_global_test_runner FOLDER usvfs_global_test) +usvfs_target_link_usvfs(usvfs_global_test_runner) +target_link_libraries(usvfs_global_test_runner + PRIVATE test_utils gtest_utils GTest::gtest_main) +add_dependencies(usvfs_global_test_runner usvfs_global_test) diff --git a/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test/usvfs_global_test.cpp b/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test/usvfs_global_test.cpp new file mode 100644 index 0000000..79c6c4c --- /dev/null +++ b/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test/usvfs_global_test.cpp @@ -0,0 +1,216 @@ +#include <gtest/gtest.h> + +#define WIN32_LEAN_AND_MEAN +#include <Windows.h> + +#include <filesystem> +#include <fstream> + +#include <boost/filesystem.hpp> + +#include <gmock/gmock-matchers.h> +#include <gtest_utils.h> + +// anonymous class that allow tests to access parameters (currently only where the +// virtualized data folder is) +// +class +{ + bool initialize(int argc, char* argv[]) + { + // extract the data path + if (argc != 2) { + std::cerr << "missing data path, aborting\n"; + return false; + } + m_data = std::filesystem::path{argv[1]}; + + if (!exists(m_data)) { + std::cerr << "data path '" << m_data.string() << "'does not exist\n"; + return false; + } + + return true; + } + + friend int main(int argc, char* argv[]); + std::filesystem::path m_data; + +public: + const auto& data() const { return m_data; } +} parameters; + +// simple guard for handle +class HandleGuard +{ + HANDLE m_handle = INVALID_HANDLE_VALUE; + +public: + HandleGuard() = default; + HandleGuard(HANDLE handle) : m_handle{handle} {} + + ~HandleGuard() { close(); } + + operator HANDLE() { return m_handle; } + + void close() + { + if (m_handle != INVALID_HANDLE_VALUE) { + ::CloseHandle(m_handle); + m_handle = INVALID_HANDLE_VALUE; + } + } +}; + +// simple function to write content to a specified path +void write_content(const std::filesystem::path& path, const std::string_view content) +{ + std::ofstream ofs{path}; + ofs << content; +} + +TEST(BasicTest, SimpleTest) +{ + const auto data = parameters.data(); + + ASSERT_TRUE(exists(data)); + ASSERT_TRUE(exists(data / "docs")); + ASSERT_TRUE(exists(data / "empty")); + ASSERT_TRUE(exists(data / "docs" / "doc.txt")); + ASSERT_TRUE(exists(data / "readme.txt")); + + // should remove mods/mod1/info.txt + ASSERT_TRUE(exists(data / "info.txt")); + remove(data / "info.txt"); + ASSERT_FALSE(exists(data / "info.txt")); + + { + // retrieve the path of data using GetFinalPathNameByHandleW() to + // compare later on + std::filesystem::path dataPathFromHandle; + { + HandleGuard hdl = CreateFileW(data.c_str(), GENERIC_READ, 0, nullptr, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + ASSERT_NE(INVALID_HANDLE_VALUE, (HANDLE)hdl); + + WCHAR filepath[1024]; + const auto length = GetFinalPathNameByHandleW( + hdl, filepath, sizeof(filepath) / sizeof(WCHAR), FILE_NAME_NORMALIZED); + ASSERT_NE(0, length); + + dataPathFromHandle = std::filesystem::path(std::wstring(filepath, length)); + } + + const auto doc_txt = data / "docs" / "doc.txt"; + HandleGuard hdl = CreateFileW(doc_txt.c_str(), GENERIC_READ, 0, nullptr, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + ASSERT_NE(INVALID_HANDLE_VALUE, (HANDLE)hdl); + + WCHAR filepath[1024]; + const auto length = GetFinalPathNameByHandleW( + hdl, filepath, sizeof(filepath) / sizeof(WCHAR), FILE_NAME_NORMALIZED); + ASSERT_NE(0, length); + + ASSERT_EQ(dataPathFromHandle / "docs" / "doc.txt", + std::filesystem::path(std::wstring(filepath, length))); + } + + { + // retrieve the path of data using GetFileInformationByHandleEx() to + // compare later on + std::filesystem::path dataPathFromHandle; + { + HandleGuard hdl = CreateFileW(data.c_str(), GENERIC_READ, 0, nullptr, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + ASSERT_NE(INVALID_HANDLE_VALUE, (HANDLE)hdl); + + char buffer[4096]; + ASSERT_TRUE( + GetFileInformationByHandleEx(hdl, FileNameInfo, buffer, sizeof(buffer))); + auto* info = reinterpret_cast<FILE_NAME_INFO*>(buffer); + + dataPathFromHandle = std::filesystem::path( + std::wstring_view(info->FileName, info->FileNameLength / 2)); + } + + const auto info_txt = data / "readme.txt"; + HandleGuard hdl = CreateFileW(info_txt.c_str(), GENERIC_READ, 0, nullptr, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + ASSERT_NE(INVALID_HANDLE_VALUE, (HANDLE)hdl); + + char buffer[4096]; + ASSERT_TRUE( + GetFileInformationByHandleEx(hdl, FileNameInfo, buffer, sizeof(buffer))); + + auto* info = reinterpret_cast<FILE_NAME_INFO*>(buffer); + + ASSERT_EQ(dataPathFromHandle / "readme.txt", + std::filesystem::path( + std::wstring_view(info->FileName, info->FileNameLength / 2))); + } +} + +TEST(BoostFilesystemTest, BoostFilesystemTest) +{ + using namespace boost::filesystem; + + const path data{parameters.data().native()}; + + std::vector<std::string> contents; + for (recursive_directory_iterator it{data}; it != recursive_directory_iterator{}; + ++it) { + contents.push_back(relative(it->path(), data).string()); + } + ASSERT_THAT(contents, ::testing::UnorderedElementsAre( + ".gitkeep", "docs", "docs\\doc.txt", "docs\\subdocs", + "docs\\subdocs\\.gitkeep")); +} + +// see https://github.com/ModOrganizer2/modorganizer/issues/2039 for context +// +TEST(RedFileSystemTest, RedFileSystemTest) +{ + const auto data = parameters.data(); + + const auto storages_path = data / "r6" / "storages"; + const auto hudpainter_path = storages_path / "HUDPainter"; + + ASSERT_TRUE(exists(hudpainter_path / "DEFAULT.json")); + + // TEST.json does not exist, so will be created in overwrite - this mainly check that + // weakly_canonical returns the path under data/ and not the actual path under + // overwrite/ + // + // this relies on the hook for NtQueryInformationFile + // + ASSERT_FALSE(exists(hudpainter_path / "TEST.json")); + ASSERT_EQ(hudpainter_path / "TEST.json", + weakly_canonical(hudpainter_path / "TEST.json")); + write_content(hudpainter_path / "TEST.json", "{}"); +} + +TEST(SkipFilesTest, SkipFilesTest) +{ + const auto data = parameters.data(); + + // file in mod1 should have been skipped + ASSERT_FALSE(exists(data / "readme.skip")); + + // docs/doc.skip should come from data, not mods1/docs/doc.skip + ASSERT_CONTENT_EQ("doc.skip in data/docs", data / "docs" / "doc.skip"); + + // write to readme.skip should create a file in overwrite + write_content(data / "readme.skip", "readme.skip in overwrite"); +} + +int main(int argc, char* argv[]) +{ + testing::InitGoogleTest(&argc, argv); + + // initialize parameters from remaining arguments + if (!parameters.initialize(argc, argv)) { + return -1; + } + + return RUN_ALL_TESTS(); +} diff --git a/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_fixture.cpp b/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_fixture.cpp new file mode 100644 index 0000000..4f6e672 --- /dev/null +++ b/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_fixture.cpp @@ -0,0 +1,205 @@ +#include "usvfs_global_test_fixture.h" + +#include <filesystem> +#include <format> +#include <fstream> + +#include <gtest/gtest.h> + +#include <stringcast.h> +#include <test_helpers.h> +#include <usvfs.h> +#include <windows_sane.h> + +// find the path to the executable that contains gtest entries +// +std::filesystem::path path_to_usvfs_global_test() +{ + return test::path_of_test_bin( + test::platform_dependant_executable("usvfs_global_test", "exe")); +} + +// path to the fixture for the given test group +// +std::filesystem::path path_to_usvfs_global_test_figures(std::wstring_view group) +{ + return test::path_of_test_fixtures() / "usvfs_global_test" / group; +} + +// spawn the an hook version of the given +// +DWORD spawn_usvfs_hooked_process( + const std::filesystem::path& app, const std::vector<std::wstring>& arguments = {}, + const std::optional<std::filesystem::path>& working_directory = {}) +{ + using namespace usvfs::shared; + + std::wstring command = app; + std::filesystem::path cwd = working_directory.value_or(app.parent_path()); + std::vector<wchar_t> env; + + if (!arguments.empty()) { + command += L" " + boost::algorithm::join(arguments, L" "); + } + + STARTUPINFO si{0}; + si.cb = sizeof(si); + PROCESS_INFORMATION pi{0}; + +#pragma warning(push) +#pragma warning(disable : 6387) + + if (!usvfsCreateProcessHooked(nullptr, command.data(), nullptr, nullptr, FALSE, 0, + nullptr, cwd.c_str(), &si, &pi)) { + test::throw_testWinFuncFailed( + "CreateProcessHooked", + string_cast<std::string>(command, CodePage::UTF8).c_str()); + } + + WaitForSingleObject(pi.hProcess, INFINITE); + + DWORD exit = 99; + if (!GetExitCodeProcess(pi.hProcess, &exit)) { + test::WinFuncFailedGenerator failed; + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + throw failed("GetExitCodeProcess"); + } + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + +#pragma warning(pop) + + return exit; +} + +bool UsvfsGlobalTest::m_force_usvfs_logs = false; + +void UsvfsGlobalTest::ForceUsvfsLogs() +{ + m_force_usvfs_logs = true; +} + +class UsvfsGlobalTest::UsvfsGuard +{ +public: + UsvfsGuard(std::string_view instance_name = "usvfs_test", bool logging = false) + : m_parameters(usvfsCreateParameters(), &usvfsFreeParameters) + { + usvfsSetInstanceName(m_parameters.get(), instance_name.data()); + usvfsSetDebugMode(m_parameters.get(), false); + usvfsSetLogLevel(m_parameters.get(), LogLevel::Debug); + usvfsSetCrashDumpType(m_parameters.get(), CrashDumpsType::None); + usvfsSetCrashDumpPath(m_parameters.get(), ""); + + usvfsInitLogging(logging); + usvfsCreateVFS(m_parameters.get()); + } + + ~UsvfsGuard() { usvfsDisconnectVFS(); } + +private: + std::unique_ptr<usvfsParameters, decltype(&usvfsFreeParameters)> m_parameters; +}; + +UsvfsGlobalTest::UsvfsGlobalTest() + : m_usvfs{std::make_unique<UsvfsGuard>()}, + m_temporary_folder{test::path_of_test_temp()} +{ + std::string name{testing::UnitTest::GetInstance()->current_test_info()->name()}; + m_group = {name.begin(), name.end()}; +} + +UsvfsGlobalTest::~UsvfsGlobalTest() +{ + CleanUp(); +} + +std::filesystem::path UsvfsGlobalTest::ActualFolder() const +{ + return m_temporary_folder; +} + +std::filesystem::path UsvfsGlobalTest::ExpectedFolder() const +{ + return path_to_usvfs_global_test_figures(m_group) / "expected"; +} + +void UsvfsGlobalTest::CleanUp() const +{ + if (exists(m_temporary_folder)) { + remove_all(m_temporary_folder); + } +} + +void UsvfsGlobalTest::PrepareFileSystem() const +{ + // cleanup in case a previous tests failed to delete its stuff + CleanUp(); + + // copy fixtures + const auto fixtures = path_to_usvfs_global_test_figures(m_group) / "source"; + if (exists(fixtures)) { + copy(fixtures, m_temporary_folder, std::filesystem::copy_options::recursive); + } +} + +void UsvfsGlobalTest::SetUpOverwrite(bool force) const +{ + if (force && !exists(m_overwrite_folder)) { + create_directory(m_overwrite_folder); + } + + if (exists(m_overwrite_folder)) { + usvfsVirtualLinkDirectoryStatic(m_overwrite_folder.c_str(), m_data_folder.c_str(), + LINKFLAG_CREATETARGET | LINKFLAG_RECURSIVE); + } +} + +void UsvfsGlobalTest::PrepareMapping() const +{ + // should not be needed, but just to be safe + usvfsClearVirtualMappings(); + + if (!exists(m_data_folder)) { + throw std::runtime_error{ + std::format("data path missing at {}", m_data_folder.string())}; + } + + if (exists(m_mods_folder)) { + for (const auto& mod : std::filesystem::directory_iterator(m_mods_folder)) { + if (!is_directory(mod)) { + continue; + } + usvfsVirtualLinkDirectoryStatic(mod.path().c_str(), m_data_folder.c_str(), + LINKFLAG_RECURSIVE); + } + } + + // by default, only create overwrite if present + SetUpOverwrite(false); +} + +int UsvfsGlobalTest::Run() const +{ + PrepareMapping(); + + const auto res = spawn_usvfs_hooked_process( + path_to_usvfs_global_test(), {std::format(L"--gtest_filter={}.*", m_group), + L"--gtest_brief=1", m_data_folder.native()}); + + // TODO: try to do this with gtest itself? + if (m_force_usvfs_logs || res != 0) { + const auto log_path = test::path_of_test_bin(m_group + L".log"); + std::ofstream os{log_path}; + std::string buffer(1024, '\0'); + std::cout << "process returned " << std::hex << res << ", usvfs logs dumped to " + << log_path.string() << '\n'; + while (usvfsGetLogMessages(buffer.data(), buffer.size(), false)) { + os << " " << buffer.c_str() << "\n"; + } + } + + return res; +} diff --git a/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_fixture.h b/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_fixture.h new file mode 100644 index 0000000..aeafc25 --- /dev/null +++ b/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_fixture.h @@ -0,0 +1,82 @@ +#pragma once + +#include <filesystem> +#include <memory> + +#include <gtest/gtest.h> + +class UsvfsGlobalTest : public testing::Test +{ +public: + // enable log mode - this will generate USVFS log file for all tests regardless of + // success or failure (default is to only generate for failure) + // + static void ForceUsvfsLogs(); + +public: + UsvfsGlobalTest(); + + void SetUp() override { PrepareFileSystem(); } + + void TearDown() override { CleanUp(); } + + ~UsvfsGlobalTest(); + + // setup the overwrite folder + // + // if force is true, force creation of the overwrite folder even if not present, this + // is useful when the overwrite is initially empty and thus cannot be committed to git + // but need to contain files after the run + // + void SetUpOverwrite(bool force = true) const; + + // run the test, return the exit code of the google test process + // + int Run() const; + + // return the path to the folder containing the expected results + // + std::filesystem::path ActualFolder() const; + + // return the path to the folder containing the expected results + // + std::filesystem::path ExpectedFolder() const; + +private: + class UsvfsGuard; + + // always generate usvfs logs + static bool m_force_usvfs_logs; + + // prepare the filesystem by copying files and folders from the relevant fixtures + // folder to the temporary folder + // + // after this operations, the temporary folder will contain + // - a data folder + // - [optional] a mods folder containing a set of folders that should be mounted + // - [optional] an overwrite folder that should be mounted as overwrite + // + void PrepareFileSystem() const; + + // prepare mapping using the given set of paths + // + void PrepareMapping() const; + + // cleanup the temporary path + // + void CleanUp() const; + + // usvfs_guard + std::unique_ptr<UsvfsGuard> m_usvfs; + + // name of GTest group (first argument of the TEST macro) to run + std::wstring m_group; + + // path to the folder containing temporary files + std::filesystem::path m_temporary_folder; + + // path to the subfolder inside the temporary folder + std::filesystem::path m_data_folder = m_temporary_folder / "data"; + std::filesystem::path m_mods_folder = m_temporary_folder / "mods"; + std::filesystem::path m_overwrite_folder = m_temporary_folder / "overwrite"; +}; diff --git a/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_runner.cpp b/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_runner.cpp new file mode 100644 index 0000000..2fa6cc8 --- /dev/null +++ b/libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_runner.cpp @@ -0,0 +1,58 @@ +#include <gtest/gtest.h> + +#include <gtest_utils.h> +#include <test_helpers.h> +#include <usvfs.h> + +#include "usvfs_global_test_fixture.h" + +TEST_F(UsvfsGlobalTest, BasicTest) +{ + ASSERT_EQ(0, Run()); + ASSERT_DIRECTORY_EQ(ExpectedFolder(), ActualFolder()); +} + +TEST_F(UsvfsGlobalTest, BoostFilesystemTest) +{ + ASSERT_EQ(0, Run()); + ASSERT_DIRECTORY_EQ(ExpectedFolder(), ActualFolder()); +} + +TEST_F(UsvfsGlobalTest, RedFileSystemTest) +{ + SetUpOverwrite(true); + ASSERT_EQ(0, Run()); + ASSERT_DIRECTORY_EQ(ExpectedFolder(), ActualFolder()); +} + +TEST_F(UsvfsGlobalTest, SkipFilesTest) +{ + SetUpOverwrite(true); + + usvfsAddSkipFileSuffix(L".skip"); + + ASSERT_EQ(0, Run()); + ASSERT_DIRECTORY_EQ(ExpectedFolder(), ActualFolder()); +} + +int main(int argc, char* argv[]) +{ + // load the USVFS DLL + // + const auto usvfs_dll = + test::path_of_usvfs_lib(test::platform_dependant_executable("usvfs", "dll")); + test::ScopedLoadLibrary loadDll(usvfs_dll.c_str()); + if (!loadDll) { + std::wcerr << L"ERROR: failed to load usvfs dll: " << usvfs_dll.c_str() << L", " + << GetLastError() << L"\n"; + return 3; + } + + testing::InitGoogleTest(&argc, argv); + + UsvfsGlobalTest::ForceUsvfsLogs(); + + usvfsInitLogging(false); + + return RUN_ALL_TESTS(); +} diff --git a/libs/usvfs/test/usvfs_test_runner/CMakeLists.txt b/libs/usvfs/test/usvfs_test_runner/CMakeLists.txt new file mode 100644 index 0000000..a0a7ca9 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.16) + +find_package(GTest CONFIG REQUIRED) + +# other executables +add_executable(test_file_operations + test_file_operations/test_file_operations.cpp + test_file_operations/test_filesystem.cpp + test_file_operations/test_filesystem.h + test_file_operations/test_ntapi.cpp + test_file_operations/test_ntapi.h + test_file_operations/test_ntdll_declarations.h + test_file_operations/test_w32api.cpp + test_file_operations/test_w32api.h +) +usvfs_set_test_properties(test_file_operations FOLDER usvfs_test_runner) +target_link_libraries(test_file_operations PRIVATE test_utils ntdll) + +add_executable(usvfs_test + usvfs_test/usvfs_basic_test.cpp + usvfs_test/usvfs_basic_test.h + usvfs_test/usvfs_test_base.cpp + usvfs_test/usvfs_test_base.h + usvfs_test/usvfs_test.cpp +) +usvfs_set_test_properties(usvfs_test FOLDER usvfs_test_runner) +usvfs_target_link_usvfs(usvfs_test) +target_link_libraries(usvfs_test PRIVATE test_utils) + +# actual test executable +add_executable(usvfs_test_runner usvfs_test_runner.cpp) +usvfs_set_test_properties(usvfs_test_runner FOLDER usvfs_test_runner) +target_link_libraries(usvfs_test_runner + PRIVATE test_utils GTest::gtest GTest::gtest_main) +add_dependencies(usvfs_test_runner usvfs_test test_file_operations) diff --git a/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_file_operations.cpp b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_file_operations.cpp new file mode 100644 index 0000000..2135df3 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_file_operations.cpp @@ -0,0 +1,496 @@ +#include <cstdio> +#include <stdexcept> + +#include <boost/filesystem.hpp> +#include <boost/type_traits.hpp> +#include <winapi.h> + +#include "test_ntapi.h" +#include "test_w32api.h" +#include <test_helpers.h> + +#define WIN32_LEAN_AND_MEAN +#include <Windows.h> + +using usvfs::shared::CodePage; +using usvfs::shared::string_cast; + +void print_usage(const char* myname) +{ + using namespace std; + fprintf(stderr, "usage: %s [<options>] <command> [<command params>] ...\n", myname); + fprintf(stderr, + "options and commands are parsed and executed in the order they appear.\n"); + fprintf(stderr, "\nsupported commands:\n"); + fprintf( + stderr, + " -list <dir> : lists the given directory and outputs the results.\n"); + fprintf(stderr, " -listcontents <dir> : lists the given directory, reading all files " + "and outputs the results.\n"); + fprintf(stderr, + " -read <file> : reads the given file and outputs the results.\n"); + fprintf(stderr, " -overwrite <file> <string> : overwrites the file at the given path " + "with the given line (creating directories if in recursive mode).\n"); + fprintf(stderr, " -deleteoverwrite <file> <string> : shorthand for -delete <file> " + "-overwrite <file> <string>\n"); + fprintf(stderr, + " -rewrite <file> <string> : rewrites the file at the given path with the " + "given line (fails if file doesn't exist; uses read/write access).\n"); + fprintf(stderr, + " -touch <file> : updates last write timestamp of specified file.\n"); + fprintf(stderr, " -touchw <file> : updates last write timestamp using full " + "write permissions.\n"); + fprintf(stderr, " -delete <file> : deletes the given file.\n"); + fprintf(stderr, " -copy <src> <dst> : copies the given file.\n"); + fprintf(stderr, " -copyover <src> <dst> : copies the given file (replacing existing " + "destination).\n"); + fprintf(stderr, " -rename <src> <dst> : renames the given file.\n"); + fprintf(stderr, " -renameover <src> <dst> : renames the given file (replacing " + "existing destination).\n"); + fprintf(stderr, " -deleterename <src> <dst> : shorthand for -delete <dst> -rename " + "<src> <dst>.\n"); + fprintf(stderr, + " -move <src> <dst> : moves the given file (not supported by ntapi).\n"); + fprintf(stderr, " -moveover <src> <dst> : moves the given file (replacing existing " + "destination; not supported by ntapi).\n"); + fprintf( + stderr, + " -deletemove <src> <dst> : shorthand for -delete <dst> -move <src> <dst>.\n"); + fprintf(stderr, " -debug : shows a message box and wait for a debugger " + "to connect.\n"); + fprintf(stderr, "\nsupported options:\n"); + fprintf(stderr, " -out <file> : file to log output to (use \"-\" for the " + "stdout; otherwise path to output should exist).\n"); + fprintf(stderr, " -out+ <file> : similar to -out but appends the file instead " + "of overwriting it.\n"); + fprintf(stderr, " -cout <file> : similar to -out but does not log PID and " + "other info which may change between runs.\n"); + fprintf(stderr, " -cout+ <file> : similar to -cout but appends the file " + "instead of overwriting it.\n"); + fprintf(stderr, " -r : recursively list/create directories.\n"); + fprintf(stderr, + " -r- : don't recursively list/create directories.\n"); + fprintf(stderr, " -basedir <dir> : any paths under the basedir will outputed in " + "a relative manner (default is current directory).\n"); + fprintf(stderr, + " -w32api : use regular Win32 API for file access (default).\n"); + fprintf(stderr, + " -ntapi : use lower level ntdll functions for file access.\n"); +} + +class CommandExecuter +{ +public: + CommandExecuter() : m_output(stdout), m_api(&w32api) + { + set_basedir(TestFileSystem::current_directory().string().c_str()); + } + + ~CommandExecuter() + { + if (m_output && m_output != stdout) + fclose(m_output); + } + + FILE* output() const { return m_output; } + + bool file_output() const { return m_output != stdout; } + + // Options: + + void set_output(const char* output_file, bool clean, bool append, const char* cmdline) + { + if (m_output && m_output != stdout) { + fprintf(m_output, "#< Output log closed.\n", output_file); + fclose(m_output); + } + + m_cleanoutput = clean; + m_output = nullptr; + errno_t err = fopen_s(&m_output, output_file, append ? "at" : "wt"); + if (err || !m_output) + test::throw_testWinFuncFailed("fopen_s", output_file, err); + else { + if (m_cleanoutput) + fprintf(m_output, "#> Output log openned for: %s\n", + clean_cmdline_heuristic(cmdline).c_str()); + else + fprintf(m_output, "#> Output log openned for (pid %d): %s\n", + GetCurrentProcessId(), cmdline); + w32api.set_output(m_output, m_cleanoutput); + ntapi.set_output(m_output, m_cleanoutput); + } + } + + bool cleanoutput() const { return m_cleanoutput; } + + void set_recursive(bool recursive) { m_recursive = recursive; } + + void set_basedir(const char* basedir) + { + w32api.set_basepath(basedir); + ntapi.set_basepath(basedir); + } + + void set_ntapi(bool enable) + { + if (enable) + m_api = &ntapi; + else + m_api = &w32api; + } + + // Commands: + + void list(const char* dir, bool read_files) + { + if (debug_pending()) + __debugbreak(); + + list_impl(m_api->real_path(dir), read_files); + } + + void read(const char* path) + { + if (debug_pending()) + __debugbreak(); + + m_api->read_file(m_api->real_path(path)); + } + + void overwrite(const char* path, const char* value) + { + if (debug_pending()) + __debugbreak(); + + const auto& real = real_path_create_if_necessary(path); + + m_api->write_file(real, value, strlen(value), true, + TestFileSystem::write_mode::overwrite); + } + + void rewrite(const char* path, const char* value) + { + if (debug_pending()) + __debugbreak(); + + const auto& real = real_path_create_if_necessary(path); + + // Use read/write access when rewriting to "simulate" the harder case where it is + // not known if the file is going to actually be changed + m_api->write_file(real, value, strlen(value), false, + TestFileSystem::write_mode::manual_truncate, true); + m_api->write_file(real, "\r\n", 2, false, TestFileSystem::write_mode::append); + } + + void touch(const char* path, bool full_write_access) + { + if (debug_pending()) + __debugbreak(); + + const auto& real = real_path_create_if_necessary(path); + + m_api->touch_file(real, full_write_access); + } + + void deletef(const char* path) + { + if (debug_pending()) + __debugbreak(); + + m_api->delete_file(m_api->real_path(path)); + } + + void rename(const char* source, const char* destination, bool replace_existing) + { + if (debug_pending()) + __debugbreak(); + + m_api->copy_file(m_api->real_path(source), m_api->real_path(destination), + replace_existing); + } + + void rename(const char* source, const char* destination, bool replace_existing, + bool allow_copy) + { + if (debug_pending()) + __debugbreak(); + + m_api->rename_file(m_api->real_path(source), m_api->real_path(destination), + replace_existing, allow_copy); + } + + void debug() { m_debug_pending = true; } + + bool debug_pending() + { + if (!m_debug_pending) + return false; + m_debug_pending = false; + if (!IsDebuggerPresent()) + MessageBoxA(NULL, "Connect a debugger and press OK to trigger a breakpoint", + "DEBUG", 0); + return IsDebuggerPresent(); + } + + // Traversal: + + void list_impl(TestFileSystem::path real, bool read_files) + { + std::vector<TestFileSystem::path> recurse; + { + auto files = m_api->list_directory(real); + fprintf(m_output, ">> Listing directory {%s}:\n", + m_api->relative_path(real).string().c_str()); + for (auto f : files) { + if (f.is_dir()) { + fprintf(m_output, "[%s] DIR (attributes 0x%x)\n", + string_cast<std::string>(f.name, CodePage::UTF8).c_str(), + f.attributes); + if (m_recursive && f.name != L"." && f.name != L"..") + recurse.push_back(real / f.name); + } else { + fprintf(m_output, "[%s] FILE (attributes 0x%x, %lld bytes)\n", + string_cast<std::string>(f.name, CodePage::UTF8).c_str(), + f.attributes, f.size); + if (read_files) + m_api->read_file(real / f.name); + } + } + } + for (auto r : recurse) + list_impl(r, read_files); + } + +private: + TestFileSystem::path real_path_create_if_necessary(const char* path) + { + auto real = m_api->real_path(path); + if (m_recursive) + try { + m_api->create_path(real.parent_path()); + } catch (const std::exception& e) { + throw std::runtime_error( + std::format("Failed to create_path [{}] : {}", + m_api->relative_path(real.parent_path()).string(), e.what())); + } + return std::move(real); + } + + std::string clean_cmdline_arg(const char* arg_start, const char* arg_end) + { + if (arg_start == arg_end) + return std::string(); + bool quoted = *arg_start == '\"' && *(arg_end - 1) == '\"'; + const char* last_sep = arg_end; + while (last_sep != arg_start && *last_sep != '\\') + --last_sep; + if (arg_end - arg_start < (quoted ? 5 : 3) || arg_start[0] == '-' || + arg_start[quoted ? 2 : 1] != ':' || last_sep == arg_start) + return std::string(arg_start, arg_end); + std::string res = quoted ? "\"" : ""; + res.append(last_sep + 1, arg_end); + return res; + } + + std::string clean_cmdline_heuristic(const char* cmdline) + { + std::string res; + bool first = true; + while (*cmdline) { + const char* end = strchr(cmdline, ' '); + if (!end) + end = cmdline + strlen(cmdline); + if (first) + first = false; + else + res.push_back(' '); + res += clean_cmdline_arg(cmdline, end); + cmdline = end; + while (*cmdline == ' ') + ++cmdline; + } + return res; + } + + FILE* m_output; + bool m_cleanoutput = false; + bool m_recursive = false; + bool m_debug_pending = false; + + TestFileSystem* m_api; + static TestW32Api w32api; + static TestNtApi ntapi; +}; + +// static +TestW32Api CommandExecuter::w32api(stdout); +TestNtApi CommandExecuter::ntapi(stdout); + +class abort_invalid_argument : std::exception +{}; + +bool verify_args_exist(const char* flag, int params, int index, int count) +{ + if (index + params >= count) { + fprintf(stderr, "ERROR: %s requires %d arguments\n", flag, params); + throw abort_invalid_argument(); + } + return true; +} + +const char* UntouchedCommandLineArguments() +{ + const char* cmd = GetCommandLineA(); + for (; *cmd && *cmd != ' '; ++cmd) { + if (*cmd == '"') { + int escaped = 0; + for (++cmd; *cmd && (*cmd != '"' || escaped % 2 != 0); ++cmd) + escaped = *cmd == '\\' ? escaped + 1 : 0; + } + } + while (*cmd == ' ') + ++cmd; + return cmd; +} + +int main(int argc, char* argv[]) +{ + bool found_commands = false; + CommandExecuter executer; + + TestFileSystem::path exe_path = argv[0]; + std::string exename = exe_path.filename().string(); + std::string cmdline = exename + " " + UntouchedCommandLineArguments(); + fprintf(stdout, "#> process %d started with commandline: %s\n", GetCurrentProcessId(), + cmdline.c_str()); + + for (int ai = 1; ai < argc; ++ai) { + try { + SetLastError(0); + + // options: + if (strcmp(argv[ai], "-out") == 0 && verify_args_exist("-out", 1, ai, argc) || + strcmp(argv[ai], "-out+") == 0 && verify_args_exist("-out+", 1, ai, argc) || + strcmp(argv[ai], "-cout") == 0 && verify_args_exist("-cout", 1, ai, argc) || + strcmp(argv[ai], "-cout+") == 0 && verify_args_exist("-cout+", 1, ai, argc)) { + bool clean = argv[ai][1] == 'c'; + bool append = argv[ai][clean ? 5 : 4] == '+'; + executer.set_output(argv[++ai], clean, append, cmdline.c_str()); + } else if (strcmp(argv[ai], "-r") == 0) + executer.set_recursive(true); + else if (strcmp(argv[ai], "-r-") == 0) + executer.set_recursive(false); + else if (strcmp(argv[ai], "-basedir") == 0 && + verify_args_exist("-basedir", 1, ai, argc)) { + executer.set_basedir(argv[++ai]); + } else if (strcmp(argv[ai], "-w32api") == 0) + executer.set_ntapi(false); + else if (strcmp(argv[ai], "-ntapi") == 0) + executer.set_ntapi(true); + // commands: + else if ((strcmp(argv[ai], "-list") == 0 || + strcmp(argv[ai], "-listcontents") == 0) && + verify_args_exist("-list", 1, ai, argc)) { + bool contents = strcmp(argv[ai], "-listcontents") == 0; + executer.list(argv[++ai], contents); + found_commands = true; + } else if (strcmp(argv[ai], "-read") == 0 && + verify_args_exist("-read", 1, ai, argc)) { + executer.read(argv[++ai]); + found_commands = true; + } else if (strcmp(argv[ai], "-overwrite") == 0 && + verify_args_exist("-overwrite", 2, ai, argc) || + strcmp(argv[ai], "-deleteoverwrite") == 0 && + verify_args_exist("-deleteoverwrite", 2, ai, argc)) { + if (argv[ai][1] == 'd') + executer.deletef(argv[ai + 1]); + executer.overwrite(argv[ai + 1], argv[ai + 2]); + ++ ++ai; + found_commands = true; + } else if (strcmp(argv[ai], "-rewrite") == 0 && + verify_args_exist("-rewrite", 2, ai, argc)) { + executer.rewrite(argv[ai + 1], argv[ai + 2]); + ++ ++ai; + found_commands = true; + } else if (strcmp(argv[ai], "-touch") == 0 && + verify_args_exist("-touch", 1, ai, argc) || + strcmp(argv[ai], "-touchw") == 0 && + verify_args_exist("-touchw", 1, ai, argc)) { + bool write_access = argv[ai][6] == 'w'; + executer.touch(argv[++ai], write_access); + found_commands = true; + } else if (strcmp(argv[ai], "-delete") == 0 && + verify_args_exist("-delete", 1, ai, argc)) { + executer.deletef(argv[++ai]); + found_commands = true; + } else if (strcmp(argv[ai], "-copy") == 0 && + verify_args_exist("-copy", 2, ai, argc) || + strcmp(argv[ai], "-copyover") == 0 && + verify_args_exist("-copyover", 2, ai, argc)) { + bool over = argv[ai][5] == 'o'; + executer.rename(argv[ai + 1], argv[ai + 2], over); + ++ ++ai; + found_commands = true; + } else if (strcmp(argv[ai], "-rename") == 0 && + verify_args_exist("-rename", 2, ai, argc) || + strcmp(argv[ai], "-renameover") == 0 && + verify_args_exist("-renameover", 2, ai, argc) || + strcmp(argv[ai], "-deleterename") == 0 && + verify_args_exist("-deleterename", 2, ai, argc) || + strcmp(argv[ai], "-move") == 0 && + verify_args_exist("-move", 2, ai, argc) || + strcmp(argv[ai], "-moveover") == 0 && + verify_args_exist("-moveover", 2, ai, argc) || + strcmp(argv[ai], "-deletemove") == 0 && + verify_args_exist("-deletemove", 2, ai, argc)) { + bool del = argv[ai][1] == 'd'; + bool move = argv[ai][del ? 7 : 1] == 'm'; + bool over = !del && argv[ai][move ? 5 : 7] == 'o'; + if (del) + executer.deletef(argv[ai + 2]); + executer.rename(argv[ai + 1], argv[ai + 2], over, move); + ++ ++ai; + found_commands = true; + } else if (strcmp(argv[ai], "-debug") == 0) { + executer.debug(); + } else { + if (executer.file_output()) + fprintf(executer.output(), "ERROR: invalid argument {%s}\n", argv[ai]); + fprintf(stderr, "ERROR: invalid argument {%s}\n", argv[ai]); + return 1; + } + } catch (const abort_invalid_argument&) { + return 1; + } +#if 1 // just a convient way to not catch exception when debugging + catch (const std::exception& e) { + if (executer.file_output()) + fprintf(executer.output(), "ERROR: %hs\n", e.what()); + fprintf(stderr, "ERROR: %hs\n", e.what()); + return 1; + } catch (...) { + if (executer.file_output()) + fprintf(executer.output(), "ERROR: unknown exception"); + fprintf(stderr, "ERROR: unknown exception\n"); + return 1; + } +#endif + } + + if (!found_commands) { + print_usage(exename.c_str()); + return 2; + } + + if (executer.file_output()) + if (executer.cleanoutput()) + fprintf(executer.output(), "#< %s ended properly.\n", exename.c_str()); + else + fprintf(executer.output(), "#< %s ended properly in process %d.\n", + exename.c_str(), GetCurrentProcessId()); + fprintf(stdout, "#< %s ended properly in process %d.\n", exename.c_str(), + GetCurrentProcessId()); + + return 0; +} diff --git a/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_filesystem.cpp b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_filesystem.cpp new file mode 100644 index 0000000..dcfd5a1 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_filesystem.cpp @@ -0,0 +1,159 @@ + +#include "test_filesystem.h" +#include <test_helpers.h> +#define WIN32_LEAN_AND_MEAN +#include <Windows.h> + +using test::throw_testWinFuncFailed; + +bool TestFileSystem::FileInformation::is_dir() const +{ + return (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0; +} + +bool TestFileSystem::FileInformation::is_file() const +{ + return (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0; +} + +TestFileSystem::TestFileSystem(FILE* output) : m_output(output) {} + +TestFileSystem::path TestFileSystem::current_directory() +{ + DWORD res = GetCurrentDirectoryW(0, NULL); + if (!res) + throw_testWinFuncFailed("GetCurrentDirectory", res); + std::wstring buf(res + 1, '\0'); + res = GetCurrentDirectoryW(buf.length(), &buf[0]); + if (!res || res >= buf.length()) + throw_testWinFuncFailed("GetCurrentDirectory", res); + buf.resize(res); + return buf; +} + +TestFileSystem::path TestFileSystem::relative_path(path full_path) +{ + return test::path_as_relative(m_basepath, full_path); +} + +// static +const char* TestFileSystem::write_operation_name(write_mode mode) +{ + switch (mode) { + case write_mode::manual_truncate: + return "Writing file (by open & truncate)"; + case write_mode::truncate: + return "Truncating file"; + case write_mode::create: + return "Creating file"; + case write_mode::overwrite: + return "Overwriting file"; + case write_mode::opencreate: + return "Opening/Creating file"; + case write_mode::append: + return "Appending file"; + } + return "Unknown write operation?!"; +} + +// static +const char* TestFileSystem::rename_operation_name(bool replace_existing, + bool allow_copy) +{ + if (allow_copy) + return replace_existing ? "Moving file over" : "Moving file"; + else + return replace_existing ? "Renaming file over" : "Renaming file"; +} + +void TestFileSystem::print_operation(const char* operation, const path& target) +{ + if (m_output) + fprintf(m_output, "# (%s) %s {%s}\n", id(), operation, + relative_path(target).string().c_str()); +} + +void TestFileSystem::print_operation(const char* operation, const path& source, + const path& target) +{ + if (m_output) + fprintf(m_output, "# (%s) %s {%s} {%s}\n", id(), operation, + relative_path(source).string().c_str(), + relative_path(target).string().c_str()); +} + +static inline void print_op_with_result(FILE* output, const char* prefix, + const char* operation, const uint32_t* result, + DWORD* last_error, const char* opt_arg) +{ + if (output) { + fprintf(output, "%s%s", prefix, operation); + if (opt_arg) + fprintf(output, " %s", opt_arg); + if (result) + fprintf(output, " returned %u (0x%x)", *result, *result); + if (last_error) + fprintf(output, " last error %u (0x%x)", *last_error, *last_error); + fprintf(output, "\n"); + } +} + +void TestFileSystem::print_result(const char* operation, uint32_t result, + bool with_last_error, const char* opt_arg, + bool hide_result) +{ + if (m_output) { + DWORD last_error = GetLastError(); + std::string prefix = "# ("; + prefix += id(); + prefix += ") "; + print_op_with_result(m_output, prefix.c_str(), operation, + hide_result ? nullptr : &result, + with_last_error ? &last_error : nullptr, opt_arg); + SetLastError(last_error); + } +} + +void TestFileSystem::print_error(const char* operation, uint32_t result, + bool with_last_error, const char* opt_arg) +{ + DWORD last_error = with_last_error ? GetLastError() : 0; + print_op_with_result(stderr, "ERROR: ", operation, &result, + with_last_error ? &last_error : nullptr, opt_arg); + if (m_output && m_output != stdout) + print_op_with_result(m_output, "ERROR: ", operation, &result, + with_last_error ? &last_error : nullptr, opt_arg); +} + +void TestFileSystem::print_write_success(const void* data, std::size_t size, + std::size_t written) +{ + if (m_output) { + fprintf(m_output, "# Successfully written %u bytes ", + static_cast<unsigned>(written)); + // heuristics to print nicer one liners: + if (size == 1 && reinterpret_cast<const char*>(data)[0] == '\n' || + size == 2 && reinterpret_cast<const char*>(data)[0] == '\r' && + reinterpret_cast<const char*>(data)[1] == '\n') + fprintf(m_output, "<newline>"); + else { + fprintf(m_output, "{"); + if (size && reinterpret_cast<const char*>(data)[size - 1] == '\n') + --size; + if (data) + fwrite(data, 1, size, m_output); + else if (size) + fwrite("NULL?!", 1, 6, m_output); + fprintf(m_output, "}"); + } + fprintf(output(), "\n"); + } +} + +uint32_t TestFileSystem::clean_attributes(uint32_t attr) +{ + if (m_cleanoutput) + return attr & ~(FILE_ATTRIBUTE_NOT_CONTENT_INDEXED); + else + return attr; +} diff --git a/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_filesystem.h b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_filesystem.h new file mode 100644 index 0000000..3a5f53b --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_filesystem.h @@ -0,0 +1,104 @@ +#pragma once + +#include <cstdio> +#include <filesystem> +#include <string> +#include <vector> + +class TestFileSystem +{ +public: + static constexpr auto FILE_CONTENTS_PRINT_PREFIX = "== "; + + typedef std::filesystem::path path; + typedef std::FILE FILE; + + static path current_directory(); + + TestFileSystem(FILE* output); + + void set_output(FILE* output, bool clean) + { + m_output = output; + m_cleanoutput = clean; + } + + // base path used to trim outputs (which is important so we can compare tests ran at + // different base paths) + void set_basepath(const char* path) { m_basepath = real_path(path); } + + // returns the path relative to the base path + path relative_path(path full_path); + + virtual const char* id() = 0; + + virtual path real_path(const char* abs_or_rel_path) = 0; + + struct FileInformation + { + std::wstring name; + uint32_t attributes; + uint64_t size; + + FileInformation(const std::wstring& iname, uint32_t iattributes, uint64_t isize) + : name(iname), attributes(iattributes), size(isize) + {} + + bool is_dir() const; + bool is_file() const; + }; + typedef std::vector<FileInformation> FileInfoList; + + virtual FileInfoList list_directory(const path& directory_path) = 0; + + virtual void create_path(const path& directory_path) = 0; + + virtual void read_file(const path& file_path) = 0; + + enum class write_mode + { + manual_truncate, + truncate, + create, + overwrite, + opencreate, + append + }; + virtual void write_file(const path& file_path, const void* data, std::size_t size, + bool add_new_line, write_mode mode, + bool rw_access = false) = 0; + + virtual void touch_file(const path& file_path, bool full_write_access = false) = 0; + + virtual void delete_file(const path& file_path) = 0; + + virtual void copy_file(const path& source_path, const path& destination_path, + bool replace_existing) = 0; + + virtual void rename_file(const path& source_path, const path& destination_path, + bool replace_existing, bool allow_copy) = 0; + +protected: + FILE* output() { return m_output; } + bool cleanoutput() const { return m_cleanoutput; } + static const char* write_operation_name(write_mode mode); + static const char* rename_operation_name(bool replace_existing, bool allow_copy); + +public: // mainly for derived class (but also used by helper classes like SafeHandle so + // public) + void print_operation(const char* operation, const path& target); + void print_operation(const char* operation, const path& source, const path& target); + void print_result(const char* operation, uint32_t result, + bool with_last_error = false, const char* opt_arg = nullptr, + bool hide_result = false); + void print_error(const char* operation, uint32_t result, bool with_last_error = false, + const char* opt_arg = nullptr); + void print_write_success(const void* data, std::size_t size, std::size_t written); + + uint32_t clean_attributes(uint32_t attr); + +private: + FILE* m_output; + bool m_cleanoutput = false; + path m_basepath; +}; diff --git a/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntapi.cpp b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntapi.cpp new file mode 100644 index 0000000..34966b5 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntapi.cpp @@ -0,0 +1,488 @@ + +#include "test_ntapi.h" +#include <cstdio> +#include <cstring> +#include <test_helpers.h> +#include <vector> + +#define WIN32_LEAN_AND_MEAN +#include <Windows.h> +#include <Winternl.h> +#include <stdio.h> + +#include "test_ntdll_declarations.h" + +class TestNtApi::SafeHandle +{ +public: + SafeHandle(TestFileSystem* tfs, HANDLE handle = NULL) : m_handle(handle), m_tfs(tfs) + {} + SafeHandle(const SafeHandle&) = delete; + SafeHandle(SafeHandle&& other) : m_handle(other.m_handle), m_tfs(other.m_tfs) + { + other.m_handle = nullptr; + } + + operator HANDLE() { return m_handle; } + operator PHANDLE() { return &m_handle; } + + bool valid() const { return m_handle != NULL; } + + ~SafeHandle() + { + if (m_handle) { + NTSTATUS status = NtClose(m_handle); + if (m_tfs) + m_tfs->print_result("NtClose", status); + if (!NT_SUCCESS(status)) + if (m_tfs) + m_tfs->print_error("NtClose", status); + else + std::fprintf(stderr, "NtClose failed : 0x%lx", status); + m_handle = NULL; + } + } + +private: + HANDLE m_handle; + TestFileSystem* m_tfs; +}; + +const char* TestNtApi::id() +{ + return "Nt"; +} + +TestNtApi::path TestNtApi::real_path(const char* abs_or_rel_path) +{ + if (!abs_or_rel_path || !abs_or_rel_path[0]) + return path(); + + static constexpr char nt_path_prefix[] = "\\??\\"; + static constexpr wchar_t nt_path_prefix_w[] = L"\\??\\"; + + bool path_dos = strncmp(abs_or_rel_path, nt_path_prefix, strlen(nt_path_prefix)) == 0; + bool path_has_drive = abs_or_rel_path[1] == ':'; + bool path_unc = abs_or_rel_path[0] == '\\' && abs_or_rel_path[1] == '\\'; + bool path_absolute = path_has_drive || abs_or_rel_path[0] == '\\'; + + path result; + if (!path_dos) { + if (!path_unc) + result.assign(nt_path_prefix_w); + if (!path_absolute) + result /= current_directory(); + else if (!path_has_drive && !path_unc) + // if "absolute" path but without a drive letter (i.e. \windows) + // the take the drive from the current directory: (i.e. "C:") + result /= current_directory().root_name(); + } + + int result_size = 0; + for (auto r : result) + ++result_size; + + // now append abs_or_rel_path, handling ".." and "." properly: + path arp{abs_or_rel_path}; + int base_size = path_unc ? 3 : 4; + for (auto p : arp) { + if (p == "..") { + if (result_size > base_size) { // refuse to remove top level element (i.e. + // \??\C:\ which is 4 elements) + result.remove_filename(); + --result_size; + } + } else if (!p.empty() && p != ".") { + result /= p; + ++result_size; + } + } + + return result; +} + +TestNtApi::SafeHandle TestNtApi::open_directory(const path& directory_path, bool create, + bool allow_non_existence, long* pstatus) +{ + print_operation(create ? "Creating directory" : "Openning directory", directory_path); + + UNICODE_STRING unicode_path; + RtlInitUnicodeString(&unicode_path, directory_path.c_str()); + + OBJECT_ATTRIBUTES attributes; + InitializeObjectAttributes(&attributes, &unicode_path, OBJ_CASE_INSENSITIVE, NULL, + NULL); + + SafeHandle dir(this); + IO_STATUS_BLOCK iosb; + NTSTATUS status = NtCreateFile( + dir, FILE_LIST_DIRECTORY | FILE_TRAVERSE | SYNCHRONIZE, &attributes, &iosb, NULL, + FILE_ATTRIBUTE_DIRECTORY, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + create ? FILE_OPEN_IF : FILE_OPEN, + FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); + + print_result("NtCreateFile", status); + + if (pstatus) + *pstatus = status; + if ((status == STATUS_OBJECT_NAME_NOT_FOUND || + status == STATUS_OBJECT_PATH_NOT_FOUND) && + allow_non_existence) + return NULL; + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtCreateFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtCreateFile", "bad iosb.Status", iosb.Status); + + return dir; +} + +TestFileSystem::FileInfoList TestNtApi::list_directory(const path& directory_path) +{ + SafeHandle dir = open_directory(directory_path, false); + + print_operation("Querying directory", directory_path); + + FileInfoList files; + while (true) { + char buf[4096]{0}; + IO_STATUS_BLOCK iosb; + + NTSTATUS status = + NtQueryDirectoryFile(dir, NULL, NULL, NULL, &iosb, buf, sizeof(buf), + MyFileBothDirectoryInformation, FALSE, NULL, FALSE); + print_result("NtQueryDirectoryFile", status); + + if (status == STATUS_NO_MORE_FILES) + break; + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtQueryDirectoryFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtQueryDirectoryFile", "bad iosb.Status", iosb.Status); + if (iosb.Information == 0) // This shouldn't happend unless the filename (not full + // path) is larger then sizeof(buf) + throw test::FuncFailed("NtQueryDirectoryFile", "buffer too small", + iosb.Information); + + PFILE_BOTH_DIR_INFORMATION info = reinterpret_cast<PFILE_BOTH_DIR_INFORMATION>(buf); + while (true) { + files.push_back(FileInformation( + std::wstring(info->FileName, + info->FileNameLength / sizeof(info->FileName[0])), + clean_attributes(info->FileAttributes), info->EndOfFile.QuadPart)); + if (info->NextEntryOffset) + info = reinterpret_cast<PFILE_BOTH_DIR_INFORMATION>( + reinterpret_cast<char*>(info) + info->NextEntryOffset); + else + break; + } + } + + return files; +} + +void TestNtApi::create_path(const path& directory_path) +{ + // sanity and guaranteed recursion end: + if (!directory_path.has_relative_path()) + throw std::runtime_error("Refusing to create non-existing top level path"); + + // if directory already exists all is good + NTSTATUS status; + if (open_directory(directory_path, false, true, &status).valid()) + return; + + if (status != STATUS_OBJECT_NAME_NOT_FOUND) // STATUS_OBJECT_NAME_NOT_FOUND means + // parent directory already exists + create_path(directory_path + .parent_path()); // otherwise create parent directory (recursively) + + open_directory(directory_path, true); +} + +void TestNtApi::read_file(const path& file_path) +{ + print_operation("Reading file", file_path); + + UNICODE_STRING unicode_path; + RtlInitUnicodeString(&unicode_path, file_path.c_str()); + + OBJECT_ATTRIBUTES attributes; + InitializeObjectAttributes(&attributes, &unicode_path, OBJ_CASE_INSENSITIVE, NULL, + NULL); + + SafeHandle file(this); + IO_STATUS_BLOCK iosb; + NTSTATUS status = NtOpenFile(file, GENERIC_READ | SYNCHRONIZE, &attributes, &iosb, + FILE_SHARE_READ, FILE_SYNCHRONOUS_IO_NONALERT); + print_result("NtOpenFile", status); + + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtOpenFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtOpenFile", "bad iosb.Status", iosb.Status); + + uint32_t total = 0; + bool ends_with_newline = true; + bool pending_prefix = true; + while (true) { + char buf[4096]; + + memset(&iosb, 0, sizeof(iosb)); + status = NtReadFile(file, NULL, NULL, NULL, &iosb, buf, sizeof(buf), NULL, NULL); + print_result("NtReadFile", status); + if (status == STATUS_END_OF_FILE) + break; + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtReadFile", status); + + total += iosb.Information; + char* begin = buf; + char* end = begin + iosb.Information; + while (begin != end) { + if (pending_prefix) { + if (output()) + fwrite(FILE_CONTENTS_PRINT_PREFIX, 1, strlen(FILE_CONTENTS_PRINT_PREFIX), + output()); + pending_prefix = false; + } + bool skip_newline = false; + char* print_end = reinterpret_cast<char*>(std::memchr(begin, '\n', end - begin)); + if (print_end) { + pending_prefix = true; + if (print_end > begin && *(print_end - 1) == '\r') { + // convert \r\n => \n: + *(print_end - 1) = '\n'; + skip_newline = true; + } else // only a '\n' so just print it + ++print_end; + } else { + print_end = end; + if (print_end > begin && *(print_end - 1) == '\r') { + // buffer ends with \r so skip it under the hope it will be followed with a \n + --print_end; + skip_newline = true; + } + } + if (output()) + fwrite(begin, 1, print_end - begin, output()); + ends_with_newline = print_end > begin && *(print_end - 1) == '\n'; + begin = print_end; + if (skip_newline) + ++begin; + } + if (output() && !ends_with_newline) { + fwrite("\n", 1, 1, output()); + ends_with_newline = true; + } + } + if (output()) { + fprintf(output(), "# Successfully read %u bytes.\n", total); + } +} + +void TestNtApi::write_file(const path& file_path, const void* data, std::size_t size, + bool add_new_line, write_mode mode, bool rw_access) +{ + print_operation(write_operation_name(mode), file_path); + + UNICODE_STRING unicode_path; + RtlInitUnicodeString(&unicode_path, file_path.c_str()); + + OBJECT_ATTRIBUTES attributes; + InitializeObjectAttributes(&attributes, &unicode_path, OBJ_CASE_INSENSITIVE, NULL, + NULL); + + ACCESS_MASK access = GENERIC_WRITE | SYNCHRONIZE; + ULONG disposition = FILE_OPEN; + switch (mode) { + case write_mode::truncate: + disposition = FILE_OVERWRITE; + break; + case write_mode::create: + disposition = FILE_CREATE; + break; + case write_mode::overwrite: + disposition = FILE_SUPERSEDE; + break; + case write_mode::opencreate: + disposition = FILE_OPEN_IF; + break; + case write_mode::append: + disposition = FILE_OPEN_IF; + access = FILE_APPEND_DATA | SYNCHRONIZE; + break; + } + if (rw_access) + access |= GENERIC_READ; + + SafeHandle file(this); + IO_STATUS_BLOCK iosb; + NTSTATUS status = + NtCreateFile(file, access, &attributes, &iosb, NULL, FILE_ATTRIBUTE_NORMAL, 0, + disposition, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); + print_result("NtCreateFile", status); + + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtCreateFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtCreateFile", "bad iosb.Status", iosb.Status); + + if (mode == write_mode::manual_truncate) { + FILE_END_OF_FILE_INFORMATION eofinfo{0}; + status = NtSetInformationFile(file, &iosb, &eofinfo, sizeof(eofinfo), + MyFileEndOfFileInformation); + print_result("NtSetInformationFile", status, false, "EOF"); + + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtSetInformationFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtSetInformationFile", "bad iosb.Status", iosb.Status); + } + + // finally write the data: + size_t total = 0; + + if (data) { + status = NtWriteFile(file, NULL, NULL, NULL, &iosb, const_cast<void*>(data), + static_cast<ULONG>(size), NULL, NULL); + print_result("NtWriteFile", status); + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtWriteFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtWriteFile", "bad iosb.Status", iosb.Status); + total += iosb.Information; + + if (add_new_line) { + char buffer[] = "\r\n"; + status = NtWriteFile(file, NULL, NULL, NULL, &iosb, buffer, 2, NULL, NULL); + print_result("NtWriteFile", status); + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtWriteFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtWriteFile", "bad iosb.Status", iosb.Status); + total += iosb.Information; + } + } + + print_write_success(data, size, total); +} + +void TestNtApi::touch_file(const path& file_path, bool full_write_access) +{ + print_operation("Touching file", file_path); + + SYSTEMTIME st; + GetSystemTime(&st); + FILETIME ft; + if (!SystemTimeToFileTime(&st, &ft)) + test::throw_testWinFuncFailed("SystemTimeToFileTime"); + + UNICODE_STRING unicode_path; + RtlInitUnicodeString(&unicode_path, file_path.c_str()); + + OBJECT_ATTRIBUTES attributes; + InitializeObjectAttributes(&attributes, &unicode_path, OBJ_CASE_INSENSITIVE, NULL, + NULL); + + SafeHandle file(this); + IO_STATUS_BLOCK iosb; + auto share_all = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; + auto access = + (full_write_access ? GENERIC_WRITE : FILE_WRITE_ATTRIBUTES) | SYNCHRONIZE; + NTSTATUS status = + NtCreateFile(file, access, &attributes, &iosb, NULL, FILE_ATTRIBUTE_NORMAL, + share_all, FILE_OPEN_IF, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); + print_result("NtCreateFile", status); + + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtCreateFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtCreateFile", "bad iosb.Status", iosb.Status); + + FILE_BASIC_INFORMATION basicinfo{0}; + basicinfo.LastWriteTime.LowPart = ft.dwLowDateTime; + basicinfo.LastWriteTime.HighPart = ft.dwHighDateTime; + status = NtSetInformationFile(file, &iosb, &basicinfo, sizeof(basicinfo), + MyFileBasicInformation); + print_result("NtSetInformationFile", status, false, "Basic"); + + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtSetInformationFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtSetInformationFile", "bad iosb.Status", iosb.Status); +} + +void TestNtApi::delete_file(const path& file_path) +{ + print_operation("Deleting file", file_path); + + UNICODE_STRING unicode_path; + RtlInitUnicodeString(&unicode_path, file_path.c_str()); + + OBJECT_ATTRIBUTES attributes; + InitializeObjectAttributes(&attributes, &unicode_path, OBJ_CASE_INSENSITIVE, NULL, + NULL); + + NTSTATUS status = NtDeleteFile(&attributes); + print_result("NtCreateFile", status); + + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtDeleteFile", status); +} + +void TestNtApi::copy_file(const path& source_path, const path& destination_path, + bool replace_existing) +{ + throw test::FuncFailed("copy_file", "ntapi does not support file copy"); +} + +void TestNtApi::rename_file(const path& source_path, const path& destination_path, + bool replace_existing, bool allow_copy) +{ + if (allow_copy) + throw test::FuncFailed("rename_file", "ntapi does not support file move"); + + print_operation(rename_operation_name(replace_existing, allow_copy), source_path, + destination_path); + + UNICODE_STRING unicode_path; + RtlInitUnicodeString(&unicode_path, source_path.c_str()); + + OBJECT_ATTRIBUTES attributes; + InitializeObjectAttributes(&attributes, &unicode_path, OBJ_CASE_INSENSITIVE, NULL, + NULL); + + SafeHandle file(this); + IO_STATUS_BLOCK iosb; + NTSTATUS status = NtCreateFile(file, FILE_READ_ATTRIBUTES | DELETE | SYNCHRONIZE, + &attributes, &iosb, NULL, FILE_ATTRIBUTE_NORMAL, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + FILE_OPEN, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); + print_result("NtCreateFile", status); + + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtCreateFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtCreateFile", "bad iosb.Status", iosb.Status); + + bool dest_full_path = source_path.parent_path() != destination_path.parent_path(); + std::wstring dest = dest_full_path ? destination_path : destination_path.filename(); + std::vector<char> buf(sizeof(FILE_RENAME_INFORMATION) + + sizeof(wchar_t) * dest.length()); + FILE_RENAME_INFORMATION* rename = + reinterpret_cast<FILE_RENAME_INFORMATION*>(buf.data()); + rename->ReplaceIfExists = replace_existing ? TRUE : FALSE; + rename->FileNameLength = sizeof(wchar_t) * dest.length(); + memcpy(&rename->FileName[0], dest.data(), sizeof(wchar_t) * dest.length()); + + status = + NtSetInformationFile(file, &iosb, rename, buf.size(), MyFileRenameInformation); + print_result("NtSetInformationFile", status, false, + dest_full_path ? "rename full path" : "rename filename"); + + if (!NT_SUCCESS(status)) + throw test::FuncFailed("NtSetInformationFile", status); + if (!NT_SUCCESS(iosb.Status)) + throw test::FuncFailed("NtSetInformationFile", "bad iosb.Status", iosb.Status); +} diff --git a/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntapi.h b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntapi.h new file mode 100644 index 0000000..fc6434e --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntapi.h @@ -0,0 +1,38 @@ +#pragma once + +#include "test_filesystem.h" + +class TestNtApi : public TestFileSystem +{ +public: + TestNtApi(FILE* output) : TestFileSystem(output) {} + + path real_path(const char* abs_or_rel_path) override; + + FileInfoList list_directory(const path& directory_path) override; + + void create_path(const path& directory_path) override; + + void read_file(const path& file_path) override; + + void write_file(const path& file_path, const void* data, std::size_t size, + bool add_new_line, write_mode mode, bool rw_access = false) override; + + void touch_file(const path& file_path, bool full_write_access = false) override; + + void delete_file(const path& file_path) override; + + void copy_file(const path& source_path, const path& destination_path, + bool replace_existing) override; + + void rename_file(const path& source_path, const path& destination_path, + bool replace_existing, bool allow_copy) override; + + const char* id() override; + +private: + class SafeHandle; + + SafeHandle open_directory(const path& directory_path, bool create, + bool allow_non_existence = false, long* pstatus = nullptr); +}; diff --git a/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntdll_declarations.h b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntdll_declarations.h new file mode 100644 index 0000000..e3799ac --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntdll_declarations.h @@ -0,0 +1,157 @@ +#pragma once + +#define STATUS_NO_MORE_FILES 0x80000006 +#define STATUS_END_OF_FILE 0xC0000011 +#define STATUS_OBJECT_NAME_NOT_FOUND 0xC0000034 +#define STATUS_OBJECT_PATH_NOT_FOUND 0xC000003A + +#define FILE_WRITE_TO_END_OF_FILE 0xffffffff +#define FILE_USE_FILE_POINTER_POSITION 0xfffffffe + +#define InitializeObjectAttributes(p, n, a, r, s) \ + { \ + (p)->Length = sizeof(OBJECT_ATTRIBUTES); \ + (p)->RootDirectory = r; \ + (p)->Attributes = a; \ + (p)->ObjectName = n; \ + (p)->SecurityDescriptor = s; \ + (p)->SecurityQualityOfService = NULL; \ + } + +// winternl.h of course defines a joke FILE_INFORMATION_CLASS (why?!) +// so added MY_ to avoid name collision but this is FILE_INFORMATION_CLASS + +typedef enum _MY_FILE_INFORMATION_CLASS +{ + MyFileDirectoryInformation = 1, + MyFileFullDirectoryInformation, + MyFileBothDirectoryInformation, + MyFileBasicInformation, + MyFileStandardInformation, + MyFileInternalInformation, + MyFileEaInformation, + MyFileAccessInformation, + MyFileNameInformation, + MyFileRenameInformation, + MyFileLinkInformation, + MyFileNamesInformation, + MyFileDispositionInformation, + MyFilePositionInformation, + MyFileFullEaInformation, + MyFileModeInformation, + MyFileAlignmentInformation, + MyFileAllInformation, + MyFileAllocationInformation, + MyFileEndOfFileInformation, + MyFileAlternateNameInformation, + MyFileStreamInformation, + MyFilePipeInformation, + MyFilePipeLocalInformation, + MyFilePipeRemoteInformation, + MyFileMailslotQueryInformation, + MyFileMailslotSetInformation, + MyFileCompressionInformation, + MyFileObjectIdInformation, + MyFileCompletionInformation, + MyFileMoveClusterInformation, + MyFileQuotaInformation, + MyFileReparsePointInformation, + MyFileNetworkOpenInformation, + MyFileAttributeTagInformation, + MyFileTrackingInformation, + MyFileIdBothDirectoryInformation, + MyFileIdFullDirectoryInformation, + MyFileValidDataLengthInformation, + MyFileShortNameInformation, + MyFileIoCompletionNotificationInformation, + MyFileIoStatusBlockRangeInformation, + MyFileIoPriorityHintInformation, + MyFileSfioReserveInformation, + MyFileSfioVolumeInformation, + MyFileHardLinkInformation, + MyFileProcessIdsUsingFileInformation, + MyFileNormalizedNameInformation, + MyFileNetworkPhysicalNameInformation, + MyFileIdGlobalTxDirectoryInformation, + MyFileIsRemoteDeviceInformation, + MyFileUnusedInformation, + MyFileNumaNodeInformation, + MyFileStandardLinkInformation, + MyFileRemoteProtocolInformation, + MyFileRenameInformationBypassAccessCheck, + MyFileLinkInformationBypassAccessCheck, + MyFileVolumeNameInformation, + MyFileIdInformation, + MyFileIdExtdDirectoryInformation, + MyFileReplaceCompletionInformation, + MyFileHardLinkFullIdInformation, + MyFileIdExtdBothDirectoryInformation, + MyFileDispositionInformationEx, + MyFileRenameInformationEx, + MyFileRenameInformationExBypassAccessCheck, + MyFileMaximumInformation +} MY_FILE_INFORMATION_CLASS, + *PMY_FILE_INFORMATION_CLASS; + +typedef struct _FILE_BOTH_DIR_INFORMATION +{ + ULONG NextEntryOffset; + ULONG FileIndex; + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + LARGE_INTEGER EndOfFile; + LARGE_INTEGER AllocationSize; + ULONG FileAttributes; + ULONG FileNameLength; + ULONG EaSize; + CCHAR ShortNameLength; + WCHAR ShortName[12]; + WCHAR FileName[1]; +} FILE_BOTH_DIR_INFORMATION, *PFILE_BOTH_DIR_INFORMATION; + +typedef struct _FILE_BASIC_INFORMATION +{ + LARGE_INTEGER CreationTime; + LARGE_INTEGER LastAccessTime; + LARGE_INTEGER LastWriteTime; + LARGE_INTEGER ChangeTime; + ULONG FileAttributes; +} FILE_BASIC_INFORMATION, *PFILE_BASIC_INFORMATION; + +typedef struct _FILE_END_OF_FILE_INFORMATION +{ + LARGE_INTEGER EndOfFile; +} FILE_END_OF_FILE_INFORMATION, *PFILE_END_OF_FILE_INFORMATION; + +typedef struct _FILE_RENAME_INFORMATION +{ + BOOLEAN ReplaceIfExists; + HANDLE RootDirectory; + ULONG FileNameLength; + WCHAR FileName[1]; +} FILE_RENAME_INFORMATION, *PFILE_RENAME_INFORMATION; + +extern "C" __kernel_entry NTSTATUS NTAPI NtQueryDirectoryFile( + IN HANDLE FileHandle, IN HANDLE Event, IN PIO_APC_ROUTINE ApcRoutine, + IN PVOID ApcContext, OUT PIO_STATUS_BLOCK IoStatusBlock, OUT PVOID FileInformation, + IN ULONG Length, IN MY_FILE_INFORMATION_CLASS FileInformationClass, + IN BOOLEAN ReturnSingleEntry, IN PUNICODE_STRING FileName, IN BOOLEAN RestartScan); + +extern "C" __kernel_entry NTSTATUS NTAPI +NtReadFile(IN HANDLE FileHandle, IN HANDLE Event, IN PIO_APC_ROUTINE ApcRoutine, + IN PVOID ApcContext, OUT PIO_STATUS_BLOCK IoStatusBlock, OUT PVOID Buffer, + IN ULONG Length, IN PLARGE_INTEGER ByteOffset, IN PULONG Key); + +extern "C" __kernel_entry NTSTATUS NTAPI +NtWriteFile(IN HANDLE FileHandle, IN HANDLE Event, IN PIO_APC_ROUTINE ApcRoutine, + IN PVOID ApcContext, OUT PIO_STATUS_BLOCK IoStatusBlock, IN PVOID Buffer, + IN ULONG Length, IN PLARGE_INTEGER ByteOffset, IN PULONG Key); + +extern "C" __kernel_entry NTSTATUS NTAPI NtSetInformationFile( + IN HANDLE FileHandle, OUT PIO_STATUS_BLOCK IoStatusBlock, IN PVOID FileInformation, + IN ULONG Length, IN MY_FILE_INFORMATION_CLASS FileInformationClass); + +extern "C" __kernel_entry NTSTATUS NTAPI +NtDeleteFile(IN POBJECT_ATTRIBUTES ObjectAttributes); diff --git a/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_w32api.cpp b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_w32api.cpp new file mode 100644 index 0000000..372a6f3 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_w32api.cpp @@ -0,0 +1,370 @@ + +#include "test_w32api.h" +#include <cstdio> +#include <cstring> +#include <test_helpers.h> + +#define WIN32_LEAN_AND_MEAN +#include <Windows.h> + +using test::throw_testWinFuncFailed; + +class TestW32Api::SafeHandle +{ +public: + SafeHandle(TestFileSystem* tfs, HANDLE handle = NULL) : m_handle(handle), m_tfs(tfs) + {} + SafeHandle(const SafeHandle&) = delete; + SafeHandle(SafeHandle&& other) : m_handle(other.m_handle), m_tfs(other.m_tfs) + { + other.m_handle = nullptr; + } + + operator HANDLE() { return m_handle; } + operator PHANDLE() { return &m_handle; } + uint32_t result_for_print() + { + return static_cast<uint32_t>(reinterpret_cast<uintptr_t>(m_handle)); + } + + bool valid() const { return m_handle != INVALID_HANDLE_VALUE; } + + ~SafeHandle() + { + if (m_handle != INVALID_HANDLE_VALUE) { + BOOL res = CloseHandle(m_handle); + if (m_tfs) + m_tfs->print_result("CloseHandle", res, true); + if (!res) + if (m_tfs) + m_tfs->print_error("CloseHandle", res, true); + else + std::fprintf(stderr, "CloseHandle failed : %d", GetLastError()); + m_handle = NULL; + } + } + +private: + HANDLE m_handle; + TestFileSystem* m_tfs; +}; + +class TestW32Api::SafeFindHandle +{ +public: + SafeFindHandle(TestFileSystem* tfs, HANDLE handle = NULL) + : m_handle(handle), m_tfs(tfs) + {} + SafeFindHandle(const SafeFindHandle&) = delete; + SafeFindHandle(SafeFindHandle&& other) : m_handle(other.m_handle), m_tfs(other.m_tfs) + { + other.m_handle = nullptr; + } + + operator HANDLE() { return m_handle; } + operator PHANDLE() { return &m_handle; } + uint32_t result_for_print() + { + return static_cast<uint32_t>(reinterpret_cast<uintptr_t>(m_handle)); + } + + bool valid() const { return m_handle != INVALID_HANDLE_VALUE; } + + ~SafeFindHandle() + { + if (m_handle != INVALID_HANDLE_VALUE) { + BOOL res = FindClose(m_handle); + if (m_tfs) + m_tfs->print_result("CloseHandle", res, true); + if (!res) + if (m_tfs) + m_tfs->print_error("CloseHandle", res, true); + else + std::fprintf(stderr, "CloseHandle failed : %d", GetLastError()); + m_handle = NULL; + } + } + +private: + HANDLE m_handle; + TestFileSystem* m_tfs; +}; + +const char* TestW32Api::id() +{ + return "W32"; +} + +TestW32Api::path TestW32Api::real_path(const char* abs_or_rel_path) +{ + if (!abs_or_rel_path || !abs_or_rel_path[0]) + return path(); + + char buf[1024]; + size_t res = GetFullPathNameA(abs_or_rel_path, _countof(buf), buf, NULL); + if (!res || res >= _countof(buf)) + throw_testWinFuncFailed("GetFullPathNameA", res); + return buf; +} + +TestFileSystem::FileInfoList TestW32Api::list_directory(const path& directory_path) +{ + print_operation("Querying directory", directory_path); + + WIN32_FIND_DATA fd; + SafeFindHandle find(this, FindFirstFileW((directory_path / L"*").c_str(), &fd)); + print_result("FindFirstFileW", 0, true, nullptr, true); + if (!find.valid()) + throw_testWinFuncFailed("FindFirstFileW"); + + FileInfoList files; + while (true) { + files.push_back( + FileInformation(fd.cFileName, clean_attributes(fd.dwFileAttributes), + fd.nFileSizeHigh * (MAXDWORD + 1) + fd.nFileSizeLow)); + BOOL res = FindNextFileW(find, &fd); + print_result("FindNextFileW", res, true); + if (!res) + break; + } + + return files; +} + +void TestW32Api::create_path(const path& directory_path) +{ + // sanity and guaranteed recursion end: + if (!directory_path.has_relative_path()) + throw std::runtime_error("Refusing to create non-existing top level path"); + + print_operation("Checking directory", directory_path); + + DWORD attr = GetFileAttributesW(directory_path.c_str()); + DWORD err = GetLastError(); + print_result("GetFileAttributesW", clean_attributes(attr), true); + if (attr != INVALID_FILE_ATTRIBUTES) { + if (attr & FILE_ATTRIBUTE_DIRECTORY) + return; // if directory already exists all is good + else + throw std::runtime_error("path exists but not a directory"); + } + if (err != ERROR_FILE_NOT_FOUND && err != ERROR_PATH_NOT_FOUND) + throw_testWinFuncFailed("GetFileAttributesW"); + + if (err != ERROR_FILE_NOT_FOUND) // ERROR_FILE_NOT_FOUND means parent directory + // already exists + create_path(directory_path + .parent_path()); // otherwise create parent directory (recursively) + + print_operation("Creating directory", directory_path); + + BOOL res = CreateDirectoryW(directory_path.c_str(), NULL); + print_result("CreateDirectoryW", res, true); + if (!res) + throw_testWinFuncFailed("CreateDirectoryW"); +} + +void TestW32Api::read_file(const path& file_path) +{ + print_operation("Reading file", file_path); + + SafeHandle file(this, CreateFileW(file_path.c_str(), GENERIC_READ, FILE_SHARE_READ, + NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL)); + print_result("CreateFileW", 0, true, nullptr, true); + if (!file.valid()) + throw_testWinFuncFailed("CreateFileW"); + + uint32_t total = 0; + bool ends_with_newline = true; + bool pending_prefix = true; + while (true) { + char buf[4096]; + + DWORD read = 0; + BOOL res = ReadFile(file, buf, sizeof(buf), &read, NULL); + print_result("ReadFile", res, true); + if (!res) + throw_testWinFuncFailed("ReadFile"); + if (!read) // eof + break; + + total += read; + char* begin = buf; + char* end = begin + read; + while (begin != end) { + if (pending_prefix) { + if (output()) + fwrite(FILE_CONTENTS_PRINT_PREFIX, 1, strlen(FILE_CONTENTS_PRINT_PREFIX), + output()); + pending_prefix = false; + } + bool skip_newline = false; + char* print_end = reinterpret_cast<char*>(std::memchr(begin, '\n', end - begin)); + if (print_end) { + pending_prefix = true; + if (print_end > begin && *(print_end - 1) == '\r') { + // convert \r\n => \n: + *(print_end - 1) = '\n'; + skip_newline = true; + } else // only a '\n' so just print it + ++print_end; + } else { + print_end = end; + if (print_end > begin && *(print_end - 1) == '\r') { + // buffer ends with \r so skip it under the hope it will be followed with a \n + --print_end; + skip_newline = true; + } + } + if (output()) + fwrite(begin, 1, print_end - begin, output()); + ends_with_newline = print_end > begin && *(print_end - 1) == '\n'; + begin = print_end; + if (skip_newline) + ++begin; + } + if (output() && !ends_with_newline) { + fwrite("\n", 1, 1, output()); + ends_with_newline = true; + } + } + if (output()) { + fprintf(output(), "# Successfully read %u bytes.\n", total); + } +} + +void TestW32Api::write_file(const path& file_path, const void* data, std::size_t size, + bool add_new_line, write_mode mode, bool rw_access) +{ + print_operation(write_operation_name(mode), file_path); + + ACCESS_MASK access = GENERIC_WRITE; + DWORD disposition = OPEN_EXISTING; + switch (mode) { + case write_mode::truncate: + disposition = TRUNCATE_EXISTING; + break; + case write_mode::create: + disposition = CREATE_NEW; + break; + case write_mode::overwrite: + disposition = CREATE_ALWAYS; + break; + case write_mode::opencreate: + disposition = OPEN_ALWAYS; + break; + case write_mode::append: + disposition = OPEN_ALWAYS; + access = FILE_APPEND_DATA; + break; + } + if (rw_access) + access |= GENERIC_READ; + + SafeHandle file(this, CreateFile(file_path.c_str(), access, 0, NULL, disposition, + FILE_ATTRIBUTE_NORMAL, NULL)); + print_result("CreateFileW", 0, true, nullptr, true); + if (!file.valid()) + throw_testWinFuncFailed("CreateFile"); + + if (mode == write_mode::manual_truncate) { + BOOL res = SetEndOfFile(file); + print_result("SetEndOfFile", res, true); + if (!res) + throw_testWinFuncFailed("SetEndOfFile"); + } + + if (mode == write_mode::append) { + DWORD res = SetFilePointer(file, 0, NULL, FILE_END); + print_result("SetFilePointer(FILE_END)", res, true); + if (res == INVALID_SET_FILE_POINTER) + throw_testWinFuncFailed("SetEndOfFile"); + } + + size_t total = 0; + + if (data) { + // finally write the data: + DWORD written = 0; + BOOL res = WriteFile(file, data, static_cast<DWORD>(size), &written, NULL); + print_result("WriteFile", written, true); + if (!res) + throw_testWinFuncFailed("WriteFile"); + total += written; + + if (add_new_line) { + res = WriteFile(file, "\r\n", 2, &written, NULL); + print_result("WriteFile", written, true, "<new line>"); + if (!res) + throw_testWinFuncFailed("WriteFile"); + total += written; + } + } + + print_write_success(data, size, total); +} + +void TestW32Api::touch_file(const path& file_path, bool full_write_access) +{ + print_operation("Touching file", file_path); + + SYSTEMTIME st; + GetSystemTime(&st); + FILETIME ft; + if (!SystemTimeToFileTime(&st, &ft)) + throw_testWinFuncFailed("SystemTimeToFileTime"); + + auto share_all = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; + auto access = full_write_access ? GENERIC_WRITE : FILE_WRITE_ATTRIBUTES; + SafeHandle file(this, CreateFile(file_path.c_str(), access, share_all, NULL, + OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL)); + print_result("CreateFileW", 0, true, nullptr, true); + if (!file.valid()) + throw_testWinFuncFailed("CreateFile"); + + BOOL res = SetFileTime(file, nullptr, nullptr, &ft); + print_result("SetFileTime", res, true); + if (!res) + throw_testWinFuncFailed("SetFileTime"); +} + +void TestW32Api::delete_file(const path& file_path) +{ + print_operation("Deleting file", file_path); + + BOOL res = DeleteFileW(file_path.c_str()); + print_result("DeleteFileW", res, true); + if (!res) + throw_testWinFuncFailed("DeleteFileW"); +} + +void TestW32Api::copy_file(const path& source_path, const path& destination_path, + bool replace_existing) +{ + print_operation(replace_existing ? "Copy file over" : "Copy file", source_path, + destination_path); + + BOOL res = + CopyFileW(source_path.c_str(), destination_path.c_str(), !replace_existing); + print_result("CopyFileW", res, true); + if (!res) + throw_testWinFuncFailed("CopyFileW"); +} + +void TestW32Api::rename_file(const path& source_path, const path& destination_path, + bool replace_existing, bool allow_copy) +{ + print_operation(rename_operation_name(replace_existing, allow_copy), source_path, + destination_path); + + DWORD flags = 0; + if (replace_existing) + flags |= MOVEFILE_REPLACE_EXISTING; + if (allow_copy) + flags |= MOVEFILE_COPY_ALLOWED; + + BOOL res = MoveFileExW(source_path.c_str(), destination_path.c_str(), flags); + print_result("MoveFileExW", res, true); + if (!res) + throw_testWinFuncFailed("MoveFileExW"); +} diff --git a/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_w32api.h b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_w32api.h new file mode 100644 index 0000000..cab8e54 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/test_file_operations/test_w32api.h @@ -0,0 +1,36 @@ +#pragma once + +#include "test_filesystem.h" + +class TestW32Api : public TestFileSystem +{ +public: + TestW32Api(FILE* output) : TestFileSystem(output) {} + + path real_path(const char* abs_or_rel_path) override; + + FileInfoList list_directory(const path& directory_path) override; + + void create_path(const path& directory_path) override; + + void read_file(const path& file_path) override; + + void write_file(const path& file_path, const void* data, std::size_t size, + bool add_new_line, write_mode mode, bool rw_access = false) override; + + void touch_file(const path& file_path, bool full_write_access = false) override; + + void delete_file(const path& file_path) override; + + void copy_file(const path& source_path, const path& destination_path, + bool replace_existing) override; + + void rename_file(const path& source_path, const path& destination_path, + bool replace_existing, bool allow_copy) override; + + const char* id() override; + +private: + class SafeHandle; + class SafeFindHandle; +}; diff --git a/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.cpp b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.cpp new file mode 100644 index 0000000..c5fb3a1 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.cpp @@ -0,0 +1,402 @@ +#include "usvfs_basic_test.h" + +const char* usvfs_basic_test::scenario_name() +{ + return SCENARIO_NAME; +} + +bool usvfs_basic_test::scenario_run() +{ + // Note: For regression purposes we don't really need to verify the results of most + // our operations as the usvfs_test_base postmortem_check will verify the final state. + // We still also verify the results here because: + // A. In some cases a later step may change the results. + // B. It is easier to understand and maintain the test when the important checks are + // together with + // their related operations. + // C. For open issues the verifications here serve as a "documentation" of the issue + // (i.e. not having + // proper copy_on_write, etc.). + + // Cases not covered by the test: + // - When a "temporary" virtualized directory is created (meaning the directory is + // created only to "shadow" + // a real directory) and is then emptied. This needs to be tested within the same + // prcoess and across different processes. + + ops_list(LR"(.)", true, true); + + // test proper path creation under overwrite when a virtualized folder is written to: + + verify_source_existance(LR"(overwrite\mfolder1)", false); + verify_source_existance(LR"(overwrite\mfolder2)", false); + verify_source_existance(LR"(overwrite\mfolder3)", false); + verify_source_existance(LR"(overwrite\mfolder4)", false); + + ops_overwrite(LR"(mfolder1\newfolder1\newfile1.txt)", + R"(newfile1.txt nonrecursive overwrite subfolder)", false); + verify_source_existance(LR"(overwrite\mfolder1\newfolder1\newfile1.txt)"); + ops_overwrite(LR"(mfolder1\newfile1.txt)", R"(newfile1.txt nonrecursive overwrite)", + false); + ops_read(LR"(mfolder1\newfile1.txt)"); + + ops_overwrite(LR"(mfolder2\newfile2.txt)", R"(newfile2.txt recursive overwrite)", + true); + ops_read(LR"(mfolder2\newfile2.txt)"); + verify_source_contents(LR"(overwrite\mfolder2\newfile2.txt)", + R"(newfile2.txt recursive overwrite)"); + ops_overwrite(LR"(mfolder2\newfile2.txt\fail)", + R"(newfile2.txt is a file so folder creation should fail)", true, + false); + verify_source_contents(LR"(overwrite\mfolder2\newfile2.txt)", + R"(newfile2.txt recursive overwrite)"); + ops_overwrite(LR"(mfolder2\mfile.txt\fail)", + R"(mfile.txt is a file so folder creation should fail)", true, false); + verify_source_existance(LR"(overwrite\mfolder2\mfile.txt)", false); + + ops_overwrite(LR"(mfolder3\newfolder3\newfile3.txt)", + R"(newfile3.txt recursive overwrite)", true); + ops_read(LR"(mfolder3\newfolder3\newfile3.txt)"); + verify_source_contents(LR"(overwrite\mfolder3\newfolder3\newfile3.txt)", + R"(newfile3.txt recursive overwrite)"); + // repeat mfolder3\newfolder3 test as that folder now exists in overwrite and that + // changes things + ops_overwrite(LR"(mfolder3\newfolder3\newfile3e.txt)", + R"(newfile3e.txt recursive overwrite)", true); + ops_read(LR"(mfolder3\newfolder3\newfile3e.txt)"); + verify_source_contents(LR"(overwrite\mfolder3\newfolder3\newfile3e.txt)", + R"(newfile3e.txt recursive overwrite)"); + + ops_overwrite(LR"(mfolder4\newfolder4\d\e\e\p\newfile4.txt)", + R"(newfile4.txt recursive overwrite)", true); + ops_read(LR"(mfolder4\newfolder4\d\e\e\p\newfile4.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4.txt)", + R"(newfile4.txt recursive overwrite)"); + // repeat mfolder4\newfolder4\d\e\e\p test as that folder now exists in overwrite and + // that changes things + ops_overwrite(LR"(mfolder4\newfolder4\d\e\e\p\newfile4e.txt)", + R"(newfile4e.txt recursive overwrite)", true); + ops_read(LR"(mfolder4\newfolder4\d\e\e\p\newfile4e.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4e.txt)", + R"(newfile4e.txt recursive overwrite)"); + // and finally verify also non-recursive works + ops_overwrite(LR"(mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)", + R"(newfile4enr.txt nonrecursive overwrite)", false); + ops_read(LR"(mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)", + R"(newfile4enr.txt nonrecursive overwrite)"); + // finally check an intermediate folder: + ops_overwrite(LR"(mfolder4\newfolder4\d\e\epnewfile4r.txt)", + R"(epnewfile4r.txt recursive overwrite)", true); + ops_read(LR"(mfolder4\newfolder4\d\e\epnewfile4r.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\epnewfile4r.txt)", + R"(epnewfile4r.txt recursive overwrite)"); + ops_overwrite(LR"(mfolder4\newfolder4\d\e\epnewfile4.txt)", + R"(epnewfile4.txt nonrecursive overwrite)", false); + ops_read(LR"(mfolder4\newfolder4\d\e\epnewfile4.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\epnewfile4.txt)", + R"(epnewfile4.txt nonrecursive overwrite)"); + + verify_mount_existance(LR"(rfolder\rcopyme4.txt)"); + verify_source_existance(LR"(overwrite\mfolder4\newfolder4p)", false); + ops_copy(LR"(rfolder\rcopyme4.txt)", LR"(mfolder4\newfolder4p\rcopyme4.txt)", true); + verify_source_existance(LR"(overwrite\mfolder4\newfolder4p\rcopyme4.txt)", true); + verify_source_existance(LR"(mod4\mfolder4\mfile.txt)"); + verify_source_existance(LR"(overwrite\mfolder4\mfile.txt)", false); + ops_copy(LR"(rfolder\rcopyme4.txt)", LR"(mfolder4\rcopyme4.txt)", false); + verify_source_existance(LR"(overwrite\mfolder4\rcopyme4.txt)"); + + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4.txt)", + R"(newfile4.txt recursive overwrite)"); + // repeat mfolder4\newfolder4\d\e\e\p test as that folder now exists in overwrite and + // that changes things + ops_overwrite(LR"(mfolder4\newfolder4\d\e\e\p\newfile4e.txt)", + R"(newfile4e.txt recursive overwrite)", true); + ops_read(LR"(mfolder4\newfolder4\d\e\e\p\newfile4e.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4e.txt)", + R"(newfile4e.txt recursive overwrite)"); + // and finally verify also non-recursive works + ops_overwrite(LR"(mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)", + R"(newfile4enr.txt nonrecursive overwrite)", false); + ops_read(LR"(mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\e\p\newfile4enr.txt)", + R"(newfile4enr.txt nonrecursive overwrite)"); + // finally check an intermediate folder: + ops_overwrite(LR"(mfolder4\newfolder4\d\e\epnewfile4r.txt)", + R"(epnewfile4r.txt recursive overwrite)", true); + ops_read(LR"(mfolder4\newfolder4\d\e\epnewfile4r.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\epnewfile4r.txt)", + R"(epnewfile4r.txt recursive overwrite)"); + ops_overwrite(LR"(mfolder4\newfolder4\d\e\epnewfile4.txt)", + R"(epnewfile4.txt nonrecursive overwrite)", false); + ops_read(LR"(mfolder4\newfolder4\d\e\epnewfile4.txt)"); + verify_source_contents(LR"(overwrite\mfolder4\newfolder4\d\e\epnewfile4.txt)", + R"(epnewfile4.txt nonrecursive overwrite)"); + + // test copy on write/delete against source "mod": + + ops_touch(LR"(root0.txt)"); + verify_source_existance(LR"(overwrite\root0.txt)", false); + ops_touch(LR"(root1.txt)"); + verify_source_existance(LR"(overwrite\root1.txt)", false); + ops_touch(LR"(root2.txt)"); + verify_source_existance(LR"(overwrite\root1.txt)", false); + ops_touch(LR"(mod1.txt)"); + verify_source_existance(LR"(overwrite\mod1.txt)", false); + ops_touch(LR"(mfolder1\mfile.txt)"); + verify_source_existance(LR"(overwrite\mfolder1\mfile.txt)", false); + + ops_touch(LR"(root0w.txt)", true); + verify_source_existance(LR"(overwrite\root0w.txt)", false); + ops_touch(LR"(root1w.txt)", true); + verify_source_existance(LR"(overwrite\root1w.txt)", false); + ops_touch(LR"(root2w.txt)", true); + verify_source_existance(LR"(overwrite\root1w.txt)", false); + ops_touch(LR"(mod1w.txt)", true); + verify_source_existance(LR"(overwrite\mod1w.txt)", false); + ops_touch(LR"(mfolder1\mfilew.txt)", true); + verify_source_existance(LR"(overwrite\mfolder1\mfilew.txt)", false); + + { + const auto& old_contents = source_contents(LR"(mod4\mfolder4\mfileoverwrite.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfileoverwrite.txt)", + old_contents.c_str()); + ops_overwrite(LR"(mfolder4\mfileoverwrite.txt)", + R"(mfolder4\mfileoverwrite.txt overwrite)", false); + ops_read(LR"(mfolder4\mfileoverwrite.txt)"); + if (bool protect_virtualized = false) { + verify_source_contents(LR"(mod4\mfolder4\mfileoverwrite.txt)", + old_contents.c_str()); + verify_source_contents(LR"(overwrite\mfolder4\mfileoverwrite.txt)", + R"(mfolder4\mfileoverwrite.txt overwrite)"); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfileoverwrite.txt)", + R"(mfolder4\mfileoverwrite.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfileoverwrite.txt)", false); + } + } + + { + const auto& old_contents = source_contents(LR"(mod4\mfolder4\mfilerewrite.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfilerewrite.txt)", old_contents.c_str()); + ops_rewrite(LR"(mfolder4\mfilerewrite.txt)", + R"(mfolder4\mfilerewrite.txt rewrite)"); + ops_read(LR"(mfolder4\mfilerewrite.txt)"); + if (auto copy_on_readwrite_implemented = false) { + verify_source_contents(LR"(mod4\mfolder4\mfilerewrite.txt)", + old_contents.c_str()); + verify_source_contents(LR"(overwrite\mfolder4\mfilerewrite.txt)", + R"(mfolder4\mfilerewrite.txt rewrite)"); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfilerewrite.txt)", + R"(mfolder4\mfilerewrite.txt rewrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfilerewrite.txt)", false); + } + } + + { + const auto& old_contents = source_contents(LR"(mod4\mfolder4\mfilemoveover.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfilemoveover.txt)", old_contents.c_str()); + ops_overwrite(LR"(mfolder4\temp_mfilemoveover.txt)", + R"(mfolder4\mfilemoveover.txt overwrite)", false); + verify_source_contents(LR"(overwrite\mfolder4\temp_mfilemoveover.txt)", + R"(mfolder4\mfilemoveover.txt overwrite)"); + ops_rename(LR"(mfolder4\temp_mfilemoveover.txt)", LR"(mfolder4\mfilemoveover.txt)", + true); + ops_read(LR"(mfolder4\mfilemoveover.txt)"); + verify_source_existance(LR"(overwrite\mfolder4\temp_mfilemoveover.txt)", false); + if (bool protect_virtualized = false) { + verify_source_contents(LR"(mod4\mfolder4\mfilemoveover.txt)", + old_contents.c_str()); + verify_source_contents(LR"(overwrite\mfolder4\mfilemoveover.txt)", + R"(mfolder4\mfilemoveover.txt overwrite)"); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfilemoveover.txt)", + R"(mfolder4\mfilemoveover.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfilemoveover.txt)", false); + } + } + + { + const auto& old_contents = + source_contents(LR"(mod4\mfolder4\mfiledeletewrite.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite.txt)", + old_contents.c_str()); + ops_deleteoverwrite(LR"(mfolder4\mfiledeletewrite.txt)", + R"(mfolder4\mfiledeletewrite.txt overwrite)", false); + ops_read(LR"(mfolder4\mfiledeletewrite.txt)"); + if (bool protect_virtualized = false) { + verify_source_contents(LR"(overwrite\mfolder4\mfiledeletewrite.txt)", + R"(mfolder4\mfiledeletewrite.txt overwrite)"); + if (auto proper_delete_implemented = false) + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite.txt)", + old_contents.c_str()); + else + verify_source_existance(LR"(mod4\mfolder4\mfiledeletewrite.txt)", false); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite.txt)", + R"(mfolder4\mfiledeletewrite.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfiledeletewrite.txt)", false); + } + } + + { + const auto& old_contents = + source_contents(LR"(mod4\mfolder4\mfiledeletewrite2p.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite2p.txt)", + old_contents.c_str()); + ops_delete(LR"(mfolder4\mfiledeletewrite2p.txt)"); + ops_overwrite(LR"(mfolder4\mfiledeletewrite2p.txt)", + R"(mfolder4\mfiledeletewrite2p.txt overwrite)", false); + ops_read(LR"(mfolder4\mfiledeletewrite2p.txt)"); + if (bool protect_virtualized_or_track_deleted_only_in_current_process = true) { + verify_source_contents(LR"(overwrite\mfolder4\mfiledeletewrite2p.txt)", + R"(mfolder4\mfiledeletewrite2p.txt overwrite)"); + if (auto proper_delete_implemented = false) + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite2p.txt)", + old_contents.c_str()); + else + verify_source_existance(LR"(mod4\mfolder4\mfiledeletewrite2p.txt)", false); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfiledeletewrite2p.txt)", + R"(mfolder4\mfiledeletewrite2p.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfiledeletewrite2p.txt)", false); + } + } + + { + const auto& old_contents = source_contents(LR"(mod4\mfolder4\mfiledeletemove.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove.txt)", + old_contents.c_str()); + ops_overwrite(LR"(mfolder4\temp_mfiledeletemove.txt)", + R"(mfolder4\mfiledeletemove.txt overwrite)", false); + verify_source_contents(LR"(overwrite\mfolder4\temp_mfiledeletemove.txt)", + R"(mfolder4\mfiledeletemove.txt overwrite)"); + ops_deleterename(LR"(mfolder4\temp_mfiledeletemove.txt)", + LR"(mfolder4\mfiledeletemove.txt)"); + ops_read(LR"(mfolder4\mfiledeletemove.txt)"); + verify_source_existance(LR"(overwrite\mfolder4\temp_mfiledeletemove.txt)", false); + if (bool protect_virtualized = false) { + verify_source_contents(LR"(overwrite\mfolder4\mfiledeletemove.txt)", + R"(mfolder4\mfiledeletemove.txt overwrite)"); + if (auto proper_delete_implemented = false) + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove.txt)", + old_contents.c_str()); + else + verify_source_existance(LR"(mod4\mfolder4\mfiledeletemove.txt)", false); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove.txt)", + R"(mfolder4\mfiledeletemove.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfiledeletemove.txt)", false); + } + } + + { + const auto& old_contents = + source_contents(LR"(mod4\mfolder4\mfiledeletemove2p.txt)"); + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove2p.txt)", + old_contents.c_str()); + ops_delete(LR"(mfolder4\mfiledeletemove2p.txt)"); + ops_overwrite(LR"(mfolder4\temp_mfiledeletemove2p.txt)", + R"(mfolder4\mfiledeletemove2p.txt overwrite)", false); + verify_source_contents(LR"(overwrite\mfolder4\temp_mfiledeletemove2p.txt)", + R"(mfolder4\mfiledeletemove2p.txt overwrite)"); + ops_rename(LR"(mfolder4\temp_mfiledeletemove2p.txt)", + LR"(mfolder4\mfiledeletemove2p.txt)", false); + ops_read(LR"(mfolder4\mfiledeletemove2p.txt)"); + verify_source_existance(LR"(overwrite\mfolder4\temp_mfiledeletemove2p.txt)", false); + if (bool protect_virtualized_or_track_deleted_only_in_current_process = true) { + verify_source_contents(LR"(overwrite\mfolder4\mfiledeletemove2p.txt)", + R"(mfolder4\mfiledeletemove2p.txt overwrite)"); + if (auto proper_delete_implemented = false) + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove2p.txt)", + old_contents.c_str()); + else + verify_source_existance(LR"(mod4\mfolder4\mfiledeletemove2p.txt)", false); + } else { + verify_source_contents(LR"(mod4\mfolder4\mfiledeletemove2p.txt)", + R"(mfolder4\mfiledeletemove2p.txt overwrite)"); + verify_source_existance(LR"(overwrite\mfolder4\mfiledeletemove2p.txt)", false); + } + } + + // test copy on write/delete/move against original mount files: + + { + const auto& old_contents = mount_contents(LR"(rfolder\rfilerewrite.txt)"); + ops_rewrite(LR"(rfolder\rfilerewrite.txt)", R"(rfolder\rfilerewrite.txt rewrite)"); + ops_read(LR"(rfolder\rfilerewrite.txt)"); + if (auto copy_on_readwrite_implemented = false) { + verify_mount_contents(LR"(rfolder\rfilerewrite.txt)", old_contents.c_str()); + verify_source_contents(LR"(overwrite\rfolder\rfilerewrite.txt)", + R"(rfolder\rfilerewrite.txt rewrite)"); + } else { + verify_mount_contents(LR"(rfolder\rfilerewrite.txt)", + R"(rfolder\rfilerewrite.txt rewrite)"); + verify_source_existance(LR"(overwrite\rfolder\rfilerewrite.txt)", false); + } + } + ops_overwrite(LR"(rfolder\rfilerewrite.txt\fail)", + R"(rfilerewrite.txt is a file so folder creation should fail)", true, + false); + verify_mount_existance( + LR"(rfolder\rfilerewrite.txt)"); // verifies its a file and not a directory + if (auto copy_on_readwrite_implemented = false) + verify_source_existance( + LR"(overwrite\rfolder\rfilerewrite.txt)"); // verifies its a file and not a + // directory + else + verify_source_existance(LR"(overwrite\rfolder\rfilerewrite.txt)", false); + ops_overwrite(LR"(rfolder\rfile0.txt\fail)", + R"(rfile0.txt is a file so folder creation should fail)", true, false); + verify_mount_existance( + LR"(rfolder\rfile0.txt)"); // verifies its a file and not a directory + + { + const auto& old_contents = mount_contents(LR"(rfolder\rfiledeletewrite.txt)"); + ops_deleteoverwrite(LR"(rfolder\rfiledeletewrite.txt)", + R"(rfolder\rfiledeletewrite.txt overwrite)", false); + ops_read(LR"(rfolder\rfiledeletewrite.txt)"); + if (bool protect_virtualized = false) { + verify_source_contents(LR"(overwrite\rfolder\rfiledeletewrite.txt)", + R"(rfolder\rfiledeletewrite.txt overwrite)"); + if (auto proper_delete_implemented = false) + verify_mount_contents(LR"(rfolder\rfiledeletewrite.txt)", old_contents.c_str()); + else + verify_mount_existance(LR"(rfolder\rfiledeletewrite.txt)", false); + } else { + verify_mount_contents(LR"(rfolder\rfiledeletewrite.txt)", + R"(rfolder\rfiledeletewrite.txt overwrite)"); + verify_source_existance(LR"(overwrite\rfolder\rfiledeletewrite.txt)", false); + } + } + + { + const auto& old_contents = mount_contents(LR"(rfolder\rfiledelete.txt)"); + ops_delete(LR"(rfolder\rfiledelete.txt)"); + ops_read(LR"(rfolder\rfiledelete.txt)", false); + if (auto proper_delete_implemented = false) + verify_mount_contents(LR"(rfolder\rfiledelete.txt)", old_contents.c_str()); + else + verify_mount_existance(LR"(rfolder\rfiledelete.txt)", false); + } + + { + const auto& old_contents = mount_contents(LR"(rfolder\rfileoldname.txt)"); + ops_rename(LR"(rfolder\rfileoldname.txt)", LR"(rfolder\rfilenewname.txt)", false, + false); + ops_read(LR"(rfolder\rfileoldname.txt)", false); + ops_read(LR"(rfolder\rfilenewname.txt)"); + verify_source_contents(LR"(overwrite\rfolder\rfilenewname.txt)", + old_contents.c_str()); + verify_mount_existance(LR"(rfolder\rfilenewname.txt)", false); + if (auto copy_on_move_implemented = false) + verify_mount_contents(LR"(rfolder\rfileoldname.txt)", old_contents.c_str()); + else + verify_mount_existance(LR"(rfolder\rfileoldname.txt)", false); + } + + ops_list(LR"(.)", true, true); + + return true; +} diff --git a/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.h b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.h new file mode 100644 index 0000000..bec81f2 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.h @@ -0,0 +1,14 @@ +#pragma once + +#include "usvfs_test_base.h" + +class usvfs_basic_test : public usvfs_test_base +{ +public: + static constexpr auto SCENARIO_NAME = "basic"; + + usvfs_basic_test(const usvfs_test_options& options) : usvfs_test_base(options) {} + + virtual const char* scenario_name(); + virtual bool scenario_run(); +}; diff --git a/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test.cpp b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test.cpp new file mode 100644 index 0000000..e6e6bb1 --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test.cpp @@ -0,0 +1,242 @@ + +#include <iostream> +#include <memory> + +#include <boost/filesystem.hpp> +#include <boost/type_traits.hpp> +#include <winapi.h> + +#include "usvfs_basic_test.h" +#include <stringcast.h> + +void print_usage(const std::wstring& exe_name, const std::wstring& test_name) +{ + using namespace std; + wcerr << "usage: " << exe_name << " [<options>] <scenario>[:<label>]" << endl; + wcerr << "available options:" << endl; + wcerr << " -ops32 : force 32bit file opertations process (default is same " + "bitness)." + << endl; + wcerr << " -ops64 : force 64bit file opertations process (default is same " + "bitness)." + << endl; + wcerr << " -opsexe <file> : full path to file operations executable (overrides " + "-ops32/64)." + << endl; + wcerr << " -opsarg <arg> : adds argument to the start of all file operations." + << endl; + wcerr << " -fixture <dir> : fixture dir (default is test\\fixtures\\" << test_name + << "\\<scenario>)." << endl; + wcerr << " -mapping <file> : mapping file (default is <fixture " + "dir>\\vfs_mappings.txt)." + << endl; + wcerr << " -temp <dir> : temp dir (default is test\\temp\\" << test_name + << "\\<scenario>_<label>)." << endl; + wcerr << " -mount <dir> : mount dir (default is <temp dir>\\mount)." << endl; + wcerr << " -source <dir> : source dir (default is <temp dir>\\source)." << endl; + wcerr << " -out <file> : output file (default is <temp " + "dir>\\<scenario>_<label>.log)." + << endl; + wcerr << " -usvfslog <file> : output file (default is <temp " + "dir>\\<scenario>_<label>_usvfs.log)." + << endl; + wcerr << " -forcetemprecursivedelete : decimate temp dir even if doesn't look like a " + "temp dir." + << endl; + wcerr << endl; + wcerr << "note: if <label> is ommited the current platform (x86/x64) is used." + << endl; + wcerr << "note: mount and source dirs should not exist or be empty directories. if " + "either of them" + << endl; + wcerr << "is not given, the temp dir is first recusrively deleted. to protect " + "against accidents a" + << endl; + wcerr << "heuristic is used to verify the temp dir only has entires which this test " + "creates." + << endl; + wcerr << "-forcetemprecursivedelete can be used to circumvent this heuristic." + << endl; +} + +usvfs_test_base* find_scenario(const std::string& scenario, + const usvfs_test_options& options) +{ + if (scenario == usvfs_basic_test::SCENARIO_NAME) + return new usvfs_basic_test(options); + else + return nullptr; +} + +bool verify_args_exist(const wchar_t* flag, int params, int index, int count) +{ + if (index + params >= count) { + std::wcerr << flag << L" requires " << params << L" arguments" << std::endl; + return false; + } else + return true; +} + +bool verify_file(const test::path& file) +{ + bool is_dir = false; + if (!winapi::ex::wide::fileExists(file.c_str(), &is_dir) || is_dir) { + std::wcerr << L"File does not exist: " << file << std::endl; + return false; + } else + return true; +} + +bool verify_dir(const test::path& dir) +{ + bool is_dir = false; + if (!winapi::ex::wide::fileExists(dir.c_str(), &is_dir) || !is_dir) { + std::wcerr << L"Directory does not exist: " << dir << std::endl; + return false; + } else + return true; +} + +std::wstring get_env_var(const wchar_t* var) +{ + using namespace std; + size_t size = GetEnvironmentVariableW(var, nullptr, 0); + if (size) { + std::wstring value(size, L'\0'); + size = GetEnvironmentVariableW(var, &value[0], static_cast<DWORD>(value.size())); + if (size && size < value.size()) { + value.resize(size); + return value; + } + } + wcerr << L"GetEnvironmentVariableW(" << var << L") failed result=" << size + << L" error=" << GetLastError() << endl; + return wstring(); +} + +bool set_env_var(const wchar_t* var, const std::wstring& value) +{ + if (SetEnvironmentVariableW(var, value.c_str())) + return true; + std::wcerr << L"SetEnvironmentVariableW(" << var << L"," << value + << L") failed error=" << GetLastError() << std::endl; + return false; +} + +int wmain(int argc, wchar_t* argv[]) +{ + using namespace std; + using namespace test; + + const wchar_t* label = nullptr; + wstring scenario; + path temp; + usvfs_test_options options; + + for (int ai = 1; ai < argc; ++ai) { + if (wcscmp(argv[ai], L"-ops32") == 0) + options.set_ops32(); + else if (wcscmp(argv[ai], L"-ops64") == 0) + options.set_ops64(); + else if (wcscmp(argv[ai], L"-opsexe") == 0) { + if (!verify_args_exist(L"-opsexe", 1, ai, argc)) + return 1; + options.opsexe = argv[++ai]; + if (!verify_file(options.opsexe)) + return 1; + } else if (wcscmp(argv[ai], L"-opsarg") == 0) { + if (!verify_args_exist(L"-opsarg", 1, ai, argc)) + return 1; + options.add_ops_options(argv[++ai]); + } else if (wcscmp(argv[ai], L"-fixture") == 0) { + if (!verify_args_exist(L"-fixture", 1, ai, argc)) + return 1; + options.fixture = argv[++ai]; + if (!verify_dir(options.fixture)) + return 1; + } else if (wcscmp(argv[ai], L"-mapping") == 0) { + if (!verify_args_exist(L"-mapping", 1, ai, argc)) + return 1; + options.mapping = argv[++ai]; + if (!verify_file(options.mapping)) + return 1; + } else if (wcscmp(argv[ai], L"-temp") == 0) { + if (!verify_args_exist(L"-temp", 1, ai, argc)) + return 1; + options.mount = argv[++ai]; + } else if (wcscmp(argv[ai], L"-mount") == 0) { + if (!verify_args_exist(L"-mount", 1, ai, argc)) + return 1; + options.mount = argv[++ai]; + } else if (wcscmp(argv[ai], L"-source") == 0) { + if (!verify_args_exist(L"-source", 1, ai, argc)) + return 1; + options.source = argv[++ai]; + } else if (wcscmp(argv[ai], L"-out") == 0) { + if (!verify_args_exist(L"-out", 1, ai, argc)) + return 1; + options.output = argv[++ai]; + } else if (wcscmp(argv[ai], L"-usvfslog") == 0) { + if (!verify_args_exist(L"-usvfslog", 1, ai, argc)) + return 1; + options.usvfs_log = argv[++ai]; + } else if (wcscmp(argv[ai], L"-forcetemprecursivedelete") == 0) + options.force_temp_cleanup = true; + else if (argv[ai][0] == '-') { + wcerr << L"Unknown option " << argv[ai] << endl; + return 1; + } else if (!scenario.empty()) { + wcerr << L"Multiple scenarios can not be specified: " << scenario.c_str() << L", " + << argv[ai] << endl; + return 1; + } else { + label = wcschr(argv[ai], ':'); + if (label) { + scenario = std::wstring(static_cast<const wchar_t*>(argv[ai]), label); + ++label; // skip the ':' + } else + scenario = argv[ai]; + + if (scenario.empty()) { + wcerr << L"Scenario name can not be empty!" << endl; + return 1; + } + } + } + + path test_name{"usvfs_test"}; + + if (scenario.empty()) { + print_usage(path(argv[0]).stem(), test_name); + return 1; + } + + options.fill_defaults(test_name, scenario, label); + + unique_ptr<usvfs_test_base> test{find_scenario( + usvfs::shared::string_cast<std::string>(scenario, usvfs::shared::CodePage::UTF8), + options)}; + if (!test) { + wcerr << L"ERROR: Unknown scenario specified: " << scenario.c_str() << endl; + return 2; + } + + auto dllPath = path_of_usvfs_lib(platform_dependant_executable("usvfs", "dll")); + ScopedLoadLibrary loadDll(dllPath.c_str()); + if (!loadDll) { + wcerr << L"ERROR: failed to load usvfs dll: " << dllPath.c_str() << L", " + << GetLastError() << endl; + return 3; + } + + // In order for bin\usvfs_proxy_x??.exe to find the lib\usvfs_x??.dll file we add the + // lib folder to our path: + std::wstring env_path = get_env_var(L"Path"); + if (env_path.empty() || + !set_env_var(L"Path", dllPath.parent_path().wstring() + L";" + env_path)) { + wcerr << L"ERROR: failed to add usvfs dll directory to path" << endl; + return 3; + } + + return test->run(path(argv[0]).stem()); +} diff --git a/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test_base.cpp b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test_base.cpp new file mode 100644 index 0000000..bfa391e --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test_base.cpp @@ -0,0 +1,1002 @@ + +#include "usvfs_test_base.h" +#include <boost/filesystem.hpp> +#include <boost/type_traits.hpp> +#include <cctype> +#include <cerrno> +#include <chrono> +#include <future> +#include <iostream> +#include <stringcast.h> +#include <thread> +#include <unordered_set> +#include <usvfs.h> +#include <vector> +#include <winapi.h> + +using test::throw_testWinFuncFailed; + +// usvfs_test_options class: + +void usvfs_test_options::fill_defaults(const path& test_name, + const std::wstring& scenario, + const wchar_t* label) +{ + using namespace test; + + std::wstring lbl; + if (label) + lbl = label; + +#ifdef _WIN64 + set_ops64(); + if (!label) + lbl = L"x64"; +#else + set_ops32(); + if (!label) + lbl = L"x86"; +#endif + + std::wstring scenario_label = scenario; + if (!lbl.empty()) { + scenario_label += L"_"; + scenario_label += lbl; + } + + if (fixture.empty()) + fixture = path_of_test_fixtures(test_name / scenario); + + if (mapping.empty()) + mapping = fixture / DEFAULT_MAPPING; + + if (temp.empty()) { + temp = path_of_test_temp(test_name / scenario_label); + } + + if (mount.empty()) { + mount = temp / MOUNT_DIR; + temp_cleanup = true; + } + + if (source.empty()) { + source = temp / SOURCE_DIR; + temp_cleanup = true; + } + + if (output.empty()) { + output = temp / scenario_label; + output += ".log"; + } + + if (usvfs_log.empty()) { + usvfs_log = temp / scenario_label; + usvfs_log += "_usvfs.log"; + } +} + +void usvfs_test_options::set_ops32() +{ + if (opsexe.empty()) + opsexe = test::path_of_test_bin(L"test_file_operations_x86.exe"); +} + +void usvfs_test_options::set_ops64() +{ + if (opsexe.empty()) + opsexe = test::path_of_test_bin(L"test_file_operations_x64.exe"); +} + +void usvfs_test_options::add_ops_options(const std::wstring& options) +{ + if (!ops_options.empty()) + ops_options += L" "; + ops_options += options; +} + +// usvfs_connector helper class: + +class usvfs_connector +{ +public: + using path = test::path; + + usvfs_connector(const usvfs_test_options& options) + : m_usvfs_log(test::ScopedFILE::open(options.usvfs_log, L"wt")), + m_exit_future(m_exit_signal.get_future()) + { + winapi::ex::wide::createPath(options.usvfs_log.parent_path().c_str()); + + std::wcout << "Connecting VFS..." << std::endl; + + std::unique_ptr<usvfsParameters, decltype(&usvfsFreeParameters)> parameters{ + usvfsCreateParameters(), &usvfsFreeParameters}; + + usvfsSetInstanceName(parameters.get(), "usvfs_test"); + usvfsSetDebugMode(parameters.get(), false); + usvfsSetLogLevel(parameters.get(), LogLevel::Debug); + usvfsSetCrashDumpType(parameters.get(), CrashDumpsType::None); + usvfsSetCrashDumpPath(parameters.get(), ""); + + usvfsInitLogging(false); + usvfsCreateVFS(parameters.get()); + + m_log_thread = std::thread(&usvfs_connector::usvfs_logger, this); + } + + ~usvfs_connector() + { + usvfsDisconnectVFS(); + m_exit_signal.set_value(); + m_log_thread.join(); + } + + enum class map_type + { + none, // the mapping_reader uses this value but will never pass it to the + // usvfs_connector (which ignored such entries) + dir, + dircreate, + file + }; + + struct mapping + { + mapping(map_type itype, std::wstring isource, std::wstring idestination) + : type(itype), source(isource), destination(idestination) + {} + + map_type type; + path source; + path destination; + }; + + using mappings_list = std::vector<mapping>; + + void updateMapping(const mappings_list& mappings, const usvfs_test_options& options, + FILE* log) + { + using namespace std; + using namespace usvfs::shared; + + fprintf(log, "Updating VFS mappings:\n"); + + usvfsClearVirtualMappings(); + + for (const auto& map : mappings) { + const string& source = + usvfs_test_base::SOURCE_LABEL + + test::path_as_relative(options.source, map.source).string(); + const string& destination = + usvfs_test_base::MOUNT_LABEL + + test::path_as_relative(options.mount, map.destination).string(); + switch (map.type) { + case map_type::dir: + fprintf(log, " mapdir: %s => %s\n", source.c_str(), destination.c_str()); + usvfsVirtualLinkDirectoryStatic(map.source.c_str(), map.destination.c_str(), + LINKFLAG_RECURSIVE); + break; + + case map_type::dircreate: + fprintf(log, " mapdircreate: %s => %s\n", source.c_str(), destination.c_str()); + usvfsVirtualLinkDirectoryStatic(map.source.c_str(), map.destination.c_str(), + LINKFLAG_CREATETARGET | LINKFLAG_RECURSIVE); + break; + + case map_type::file: + fprintf(log, " mapfile: %s => %s\n", source.c_str(), destination.c_str()); + usvfsVirtualLinkFile(map.source.c_str(), map.destination.c_str(), + LINKFLAG_RECURSIVE); + break; + } + } + + fprintf(log, "\n"); + } + + static DWORD spawn(wchar_t* commandline) + { + using namespace usvfs::shared; + + STARTUPINFO si{0}; + si.cb = sizeof(si); + PROCESS_INFORMATION pi{0}; + + if (!usvfsCreateProcessHooked(NULL, commandline, NULL, NULL, FALSE, 0, NULL, NULL, + &si, &pi)) + throw_testWinFuncFailed( + "CreateProcessHooked", + string_cast<std::string>(commandline, CodePage::UTF8).c_str()); + + WaitForSingleObject(pi.hProcess, INFINITE); + + DWORD exit = 99; + if (!GetExitCodeProcess(pi.hProcess, &exit)) { + test::WinFuncFailedGenerator failed; + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + throw failed("GetExitCodeProcess"); + } + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + return exit; + } + + void usvfs_logger() + { + fprintf(m_usvfs_log, "usvfs_test usvfs logger started:\n"); + fflush(m_usvfs_log); + + constexpr size_t size = 1024; + char buf[size + 1]{0}; + int noLogCycles = 0; + std::chrono::milliseconds wait_for; + do { + if (usvfsGetLogMessages(buf, size, false)) { + fwrite(buf, 1, strlen(buf), m_usvfs_log); + fwrite("\n", 1, 1, m_usvfs_log); + fflush(m_usvfs_log); + noLogCycles = 0; + } else + ++noLogCycles; + if (noLogCycles) + wait_for = std::chrono::milliseconds(std::min(40, noLogCycles) * 5); + else + wait_for = std::chrono::milliseconds(0); + } while (m_exit_future.wait_for(wait_for) == std::future_status::timeout); + + while (usvfsGetLogMessages(buf, size, false)) { + fwrite(buf, 1, strlen(buf), m_usvfs_log); + fwrite("\n", 1, 1, m_usvfs_log); + } + + fprintf(m_usvfs_log, "usvfs log closed.\n"); + m_usvfs_log.close(); + } + +private: + test::ScopedFILE m_usvfs_log; + std::thread m_log_thread; + std::promise<void> m_exit_signal; + std::shared_future<void> m_exit_future; +}; + +// mappings_reader + +class mappings_reader +{ +public: + using path = test::path; + using string = std::string; + using wstring = std::wstring; + using map_type = usvfs_connector::map_type; + using mapping = usvfs_connector::mapping; + using mappings_list = usvfs_connector::mappings_list; + + mappings_reader(const path& mount_base, const path& source_base) + : m_mount_base(mount_base), m_source_base(source_base) + {} + + mappings_list read(const path& mapfile) + { + const auto map = test::ScopedFILE::open(mapfile, L"rt"); + mappings_list mappings; + + char line[1024]; + while (!feof(map)) { + // read one line: + if (!fgets(line, _countof(line), map)) + if (feof(map)) + break; + else + throw_testWinFuncFailed("fgets", "reading mappings"); + + if (empty_line(line)) + continue; + + if (start_nesting(line, "mapdir")) + m_nesting = map_type::dir; + else if (start_nesting(line, "mapdircreate")) + m_nesting = map_type::dircreate; + else if (start_nesting(line, "mapfile")) + m_nesting = map_type::file; + else if (!isspace(*line)) // mapping sources should be indented and we already + // check all the mapping directives + throw test::FuncFailed("mappings_reader::read", "invalid mappings file line", + line); + else { + const auto& source_rel = trimmed_wide_string(line); + mappings.push_back(mapping(m_nesting, m_source_base / source_rel, m_mount)); + } + } + + return mappings; + } + + bool start_nesting(const char* line, const char* directive) + { + // check if line starts with directive and if so skip it: + auto dlen = strlen(directive); + auto after = line + dlen; + if (strncmp(directive, line, dlen) == 0 && (!*after || isspace(*after))) { + m_mount = m_mount_base; + const auto& mount_rel = trimmed_wide_string(after); + if (!mount_rel.empty()) + m_mount /= mount_rel; + return true; + } else + return false; + } + + static wstring trimmed_wide_string(const char* in) + { + while (std::isspace(*in)) + ++in; + auto end = in; + end += strlen(end); + while (end > in && std::isspace(*(end - 1))) + --end; + return usvfs::shared::string_cast<wstring>(string(in, end), + usvfs::shared::CodePage::UTF8); + } + + static bool empty_line(const char* line) + { + for (; *line; ++line) { + if (*line == '#') // comment, ignore rest of line + return true; + else if (!std::isspace(*line)) + return false; + } + return true; + } + +private: + path m_mount_base; + path m_source_base; + path m_mount; + map_type m_nesting = map_type::none; +}; + +// usvfs_test_base class: + +void usvfs_test_base::cleanup_temp() +{ + using namespace test; + using namespace winapi::ex::wide; + + bool isDir = false; + if (!m_o.temp_cleanup || !fileExists(m_o.temp.c_str(), &isDir)) + return; + + if (!isDir) { + if (m_o.force_temp_cleanup) + delete_file(m_o.temp); + else + throw FuncFailed("cleanup_temp", "temp exists but is a file", + m_o.temp.string().c_str()); + } else { + std::vector<wstring> cleanfiles; + std::vector<wstring> cleandirs; + std::vector<wstring> otherdirs; + bool output_file = false; + for (auto f : quickFindFiles(m_o.temp.c_str(), L"*")) + if (f.fileName == L"." || f.fileName == L"..") + continue; + else if ((f.attributes & FILE_ATTRIBUTE_DIRECTORY) == 0) { + if (f.fileName == m_o.output.filename()) + output_file = true; + cleanfiles.push_back(f.fileName); + } else if (f.fileName == SOURCE_DIR || f.fileName == MOUNT_DIR) + cleandirs.push_back(f.fileName); + else + otherdirs.push_back(f.fileName); + if (!cleanfiles.empty() || !cleandirs.empty() || !otherdirs.empty()) { + if (!m_o.force_temp_cleanup && !otherdirs.empty()) + throw FuncFailed("cleanup_temp", + "Refusing to clean temp dir with non-mount/source directories " + "(clean manually and rerun)", + m_o.temp.string().c_str()); + if (!m_o.force_temp_cleanup && cleandirs.empty() && !output_file) + throw FuncFailed("cleanup_temp", + "Refusing to clean temp dir with no directories and no output " + "log (clean manually and rerun)", + m_o.temp.string().c_str()); + std::wcout << "Cleaning previous temp dir: " << m_o.temp.c_str() << std::endl; + for (auto f : cleanfiles) + delete_file(m_o.temp / f); + for (auto d : cleandirs) + recursive_delete_files(m_o.temp / d); + for (auto d : otherdirs) + recursive_delete_files(m_o.temp / d); + } + } +} + +void usvfs_test_base::copy_fixture() +{ + using namespace test; + using namespace winapi::ex::wide; + + path fmount = m_o.fixture / MOUNT_DIR; + path fsource = m_o.fixture / SOURCE_DIR; + + bool isDir = false; + if (!fileExists(fmount.c_str(), &isDir) || !isDir) + throw FuncFailed("copy_fixture", "fixtures dir does not exist", + fmount.string().c_str()); + if (!fileExists(fsource.c_str(), &isDir) || !isDir) + throw FuncFailed("copy_fixture", "fixtures dir does not exist", + fsource.string().c_str()); + if (fileExists(m_o.mount.c_str(), &isDir)) + throw FuncFailed("copy_fixture", "source dir already exists", + m_o.mount.string().c_str()); + if (fileExists(m_o.source.c_str(), &isDir)) + throw FuncFailed("copy_fixture", "source dir already exists", + m_o.source.string().c_str()); + + std::wcout << "Copying fixture: " << m_o.fixture << std::endl; + recursive_copy_files(fmount, m_o.mount, false); + recursive_copy_files(fsource, m_o.source, false); +} + +bool usvfs_test_base::postmortem_check() +{ + path gold_output = m_o.fixture / m_o.output.filename(); + + { + const auto& log = output(); + + path mount_gold = MOUNT_DIR; + mount_gold += POSTMORTEM_SUFFIX; + path source_gold = SOURCE_DIR; + source_gold += POSTMORTEM_SUFFIX; + + bool is_dir = false; + if (!winapi::ex::wide::fileExists(m_o.mount.c_str(), &is_dir) || !is_dir) { + fprintf(log, " ERROR: mount directory does not exist?!\n"); + return false; + } + if (!winapi::ex::wide::fileExists(m_o.source.c_str(), &is_dir) || !is_dir) { + fprintf(log, " ERROR: source directory does not exist?!\n"); + return false; + } + if (!winapi::ex::wide::fileExists((m_o.fixture / mount_gold).c_str(), &is_dir) || + !is_dir) { + fprintf(log, " ERROR: fixtures golden mount does not exist: %s\n", + mount_gold.string().c_str()); + return false; + } + if (!winapi::ex::wide::fileExists((m_o.fixture / source_gold).c_str(), &is_dir) || + !is_dir) { + fprintf(log, " ERROR: fixtures golden source does not exist: %s\n", + mount_gold.string().c_str()); + return false; + } + if (!winapi::ex::wide::fileExists(gold_output.c_str(), &is_dir) || is_dir) { + fprintf(log, " ERROR: golden scenario output does not exist: %s\n", + gold_output.filename().string().c_str()); + return false; + } + + fprintf(log, "postmortem check of %s against golden %s...\n", + m_o.mount.filename().string().c_str(), mount_gold.string().c_str()); + bool mount_check = + recursive_compare_dirs(path(), m_o.fixture / mount_gold, m_o.mount, log); + + fprintf(log, "postmortem check of %s against golden %s...\n", + m_o.source.filename().string().c_str(), source_gold.string().c_str()); + bool source_check = + recursive_compare_dirs(path(), m_o.fixture / source_gold, m_o.source, log); + + if (mount_check && source_check) + fprintf(log, "postmortem check successful.\n"); + else { + fprintf(log, "ERROR: postmortem check failed!\n"); + return false; + } + } // close output before comparing it + + // don't print anything more to the output (except maybe errors), + // so that the final output can be copied as is to the fixtures (when updating the + // golden version) + + // block remove 2024/06/07 - one would need to regenerate a gold output for this but + // I am not sure this is still relevant + // + // if (!test::compare_files(gold_output, m_o.output, false)) { + // fprintf(output(), "ERROR: output does not match gold output: %s\n", + // m_o.output.filename().string().c_str()); return false; + //} + + return true; +} + +bool usvfs_test_base::recursive_compare_dirs(path rel_path, path gold_base, + path result_base, FILE* log) +{ + path result_full = result_base / rel_path; + path gold_full = gold_base / rel_path; + + std::unordered_set<std::wstring> gold_dirs; + std::unordered_set<std::wstring> gold_files; + for (const auto& f : winapi::ex::wide::quickFindFiles(gold_full.c_str(), L"*")) { + if (f.fileName == L"." || f.fileName == L"..") + continue; + if (f.attributes & FILE_ATTRIBUTE_DIRECTORY) + gold_dirs.insert(f.fileName); + else + gold_files.insert(f.fileName); + } + + bool all_good = true; + + std::vector<std::wstring> recurse; + for (const auto& f : winapi::ex::wide::quickFindFiles(result_full.c_str(), L"*")) { + if (f.fileName == L"." || f.fileName == L"..") + continue; + if (f.attributes & FILE_ATTRIBUTE_DIRECTORY) { + const auto& find = gold_dirs.find(f.fileName); + if (find != gold_dirs.end()) { + gold_dirs.erase(find); + recurse.push_back(f.fileName); + } else { + fprintf(log, " unexpected directory found: %s%s\n", MOUNT_LABEL, + (rel_path / f.fileName).string().c_str()); + all_good = false; + } + } else { + const auto& find = gold_files.find(f.fileName); + if (find != gold_files.end()) { + gold_files.erase(find); + if (!test::compare_files(gold_full / f.fileName, result_full / f.fileName, + false)) { + fprintf(log, " file contents differs: %s%s\n", MOUNT_LABEL, + (rel_path / f.fileName).string().c_str()); + all_good = false; + } + } else { + fprintf(log, " unexpected file found: %s%s\n", MOUNT_LABEL, + (rel_path / f.fileName).string().c_str()); + all_good = false; + } + } + } + + for (auto d : gold_dirs) { + fprintf(log, " expected directory not found: %s%s\n", MOUNT_LABEL, + (rel_path / d).string().c_str()); + all_good = false; + } + + for (auto f : gold_files) { + fprintf(log, " expected file not found: %s%s\n", MOUNT_LABEL, + (rel_path / f).string().c_str()); + all_good = false; + } + + for (auto r : recurse) + all_good &= recursive_compare_dirs(rel_path / r, gold_base, result_base, log); + + return all_good; +} + +test::ScopedFILE usvfs_test_base::output() +{ + auto log = test::ScopedFILE::open(m_o.output, m_clean_output ? L"wt" : L"at"); + m_clean_output = false; + return log; +} + +void usvfs_test_base::clean_output() +{ + using namespace std; + + errno_t err; + auto in = test::ScopedFILE::open(m_o.output, L"rt", err); + if (err == ENOENT) { + wcerr << L"warning: no " << m_o.output << L" to clean." << endl; + return; + } else if (err || !in) + throw_testWinFuncFailed("_wfopen_s", m_o.output.string().c_str(), err); + + path clean = m_o.output.parent_path() / m_o.output.stem(); + clean += OUTPUT_CLEAN_SUFFIX; + clean += m_o.output.extension(); + + auto out = test::ScopedFILE::open(clean, L"wt"); + wcout << L"Cleaning " << m_o.output << " to " << clean << endl; + + char line[1024]; + while (!feof(in)) { + // read one line: + if (!fgets(line, _countof(line), in)) + if (feof(in)) + break; + else + throw_testWinFuncFailed("fgets", "reading output"); + if (*line == '#') + continue; + + // in order for the clean output to compare cleanly between run with different + // options we clean out things like the platform and the ops log name (which + // contians the scenario label): + + char* platform = line; + while (platform) { + char* platform_x86 = strstr(platform, "x86"); + char* platform_x64 = strstr(platform, "x64"); + if (platform_x86 && platform_x64) + platform = std::min(platform_x86, platform_x64); + else if (platform_x86) + platform = platform_x86; + else if (platform_x64) + platform = platform_x64; + else + platform = nullptr; + if (platform) { + platform[1] = platform[2] = '?'; + platform += 3; + } + } + + char* cout_end = strstr(line, "-cout+ "); + char* cout_log_end = nullptr; + if (cout_end) { + cout_end += strlen("-cout+ "); + cout_log_end = strchr(cout_end, ' '); + } + if (cout_log_end && cout_log_end > cout_end) { + cout_end[0] = '?'; + if (cout_log_end > cout_end + 1) + memmove(cout_end + 1, cout_log_end, strlen(cout_log_end) + 1); + } + + fputs(line, out); + } +} + +int usvfs_test_base::run(const std::wstring& exe_name) +{ + using namespace usvfs::shared; + using namespace std; + + int res = run_impl(exe_name); + try { + clean_output(); + } catch (const exception& e) { + wcerr << "CERROR: " << string_cast<wstring>(e.what(), CodePage::UTF8).c_str() + << endl; + } catch (...) { + wcerr << "CERROR: unknown exception" << endl; + } + if (!res) + wcout << "scenario " << scenario_name() << " PASSED." << endl; + else + wcerr << "scenario " << scenario_name() << " FAILED!" << endl; + return res; +} + +int usvfs_test_base::run_impl(const std::wstring& exe_name) +{ + using namespace usvfs::shared; + using namespace std; + + try { + winapi::ex::wide::createPath(m_o.output.parent_path().c_str()); + + // we read mappings first only because it is "non-destructive" but might raise an + // error if mappings invalid + auto mappings = mappings_reader(m_o.mount, m_o.source).read(m_o.mapping); + + cleanup_temp(); + log_settings(exe_name); + copy_fixture(); + + usvfs_connector usvfs(m_o); + { + const auto& log = output(); + usvfs.updateMapping(mappings, m_o, log); + + fprintf(log, "running scenario %s:\n\n", scenario_name()); + } + auto res = scenario_run(); + { + const auto& log = output(); + if (res) + fprintf(log, "\nscenario ended successfully!\n\n"); + else + fprintf(log, "\nscenario failed miserably.\n"); + } + + if (!res) + return 7; + + if (!postmortem_check()) + return 8; + + return 0; + } +#if 1 // just a convient way to not catch exception when debugging + catch (const exception& e) { + try { + wcerr << "ERROR: " << string_cast<wstring>(e.what(), CodePage::UTF8).c_str() + << endl; + fprintf(output(), "ERROR: %s\n", e.what()); + } catch (const exception& e) { + wcerr << "ERROR^2: " << string_cast<wstring>(e.what(), CodePage::UTF8).c_str() + << endl; + } catch (...) { + wcerr << "ERROR^2: unknown exception" << endl; + } + } catch (...) { + try { + wcerr << "ERROR: unknown exception" << endl; + fprintf(output(), "ERROR: unknown exception\n"); + } catch (const exception& e) { + wcerr << "ERROR^2: " << string_cast<wstring>(e.what(), CodePage::UTF8).c_str() + << endl; + } catch (...) { + wcerr << "ERROR^2: unknown exception" << endl; + } + } +#else + catch (bool) { + } +#endif + return 9; // exception +} + +void usvfs_test_base::log_settings(const std::wstring& exe_name) +{ + using namespace usvfs::shared; + fprintf(output(), "%s %s started with %s%s%s\n\n", + string_cast<std::string>(exe_name).c_str(), scenario_name(), + m_o.opsexe.filename().string().c_str(), m_o.ops_options.empty() ? "" : " ", + string_cast<std::string>(m_o.ops_options).c_str()); +} + +void usvfs_test_base::ops_list(const path& rel_path, bool recursive, bool with_contents, + bool should_succeed, const wstring& additional_args) +{ + wstring cmd = recursive ? L"-r -list" : L"-list"; + if (with_contents) + cmd += L"contents"; + run_ops(should_succeed, cmd, rel_path, additional_args); +} + +void usvfs_test_base::ops_read(const path& rel_path, bool should_succeed, + const wstring& additional_args) +{ + run_ops(should_succeed, L"-read", rel_path, additional_args); +} + +void usvfs_test_base::ops_rewrite(const path& rel_path, const char* contents, + bool should_succeed, const wstring& additional_args) +{ + using namespace usvfs::shared; + run_ops(should_succeed, L"-rewrite", rel_path, additional_args, + L"\"" + string_cast<wstring>(contents, CodePage::UTF8) + L"\""); +} + +void usvfs_test_base::ops_overwrite(const path& rel_path, const char* contents, + bool recursive, bool should_succeed, + const wstring& additional_args) +{ + using namespace usvfs::shared; + run_ops(should_succeed, recursive ? L"-r -overwrite" : L"-overwrite", rel_path, + additional_args, + L"\"" + string_cast<wstring>(contents, CodePage::UTF8) + L"\""); +} + +void usvfs_test_base::ops_touch(const path& rel_path, bool full_write_access, + bool should_succeed, const wstring& additional_args) +{ + run_ops(should_succeed, full_write_access ? L"-touchw" : L"-touch", rel_path, + additional_args); +} + +void usvfs_test_base::ops_deleteoverwrite(const path& rel_path, const char* contents, + bool recursive, bool should_succeed, + const wstring& additional_args) +{ + using namespace usvfs::shared; + run_ops(should_succeed, recursive ? L"-r -deleteoverwrite" : L"-deleteoverwrite", + rel_path, additional_args, + L"\"" + string_cast<wstring>(contents, CodePage::UTF8) + L"\""); +} + +void usvfs_test_base::ops_delete(const path& rel_path, bool should_succeed, + const wstring& additional_args) +{ + run_ops(should_succeed, L"-delete", rel_path, additional_args); +} + +void usvfs_test_base::ops_copy(const path& src_rel_path, const path& dest_rel_path, + bool replace, bool should_succeed, + const wstring& additional_args) +{ + run_ops(should_succeed, replace ? L"-copyover" : L"-copy", src_rel_path, + additional_args, wstring(), dest_rel_path); +} + +void usvfs_test_base::ops_rename(const path& src_rel_path, const path& dest_rel_path, + bool replace, bool allow_copy, bool should_succeed, + const wstring& additional_args) +{ + wstring command = allow_copy ? L"-move" : L"-rename"; + if (replace) + command += L"over"; + run_ops(should_succeed, command, src_rel_path, additional_args, wstring(), + dest_rel_path); +} + +void usvfs_test_base::ops_deleterename(const path& src_rel_path, + const path& dest_rel_path, bool allow_copy, + bool should_succeed, + const wstring& additional_args) +{ + wstring command = allow_copy ? L"-deletemove" : L"-deleterename"; + run_ops(should_succeed, command, src_rel_path, additional_args, wstring(), + dest_rel_path); +} + +void usvfs_test_base::run_ops(bool should_succeed, const wstring& preargs, + const path& rel_path, const wstring& additional_args, + const wstring& postargs, const path& rel_path2) +{ + using namespace usvfs::shared; + using string = std::string; + using wstring = wstring; + + string commandlog = test::path(m_o.opsexe).filename().string(); + wstring commandline = m_o.opsexe; + if (commandline.find(' ') != wstring::npos && + commandline.find('"') == wstring::npos) { + commandline = L"\"" + commandline + L"\""; + commandlog = "\"" + commandlog + "\""; + } + + if (!m_o.mount.empty()) { + commandline += L" -basedir "; + commandline += m_o.mount; + commandlog += " -basedir "; + commandlog += m_o.mount.filename().string(); + } + + if (!m_o.ops_options.empty()) { + commandline += L" "; + commandline += m_o.ops_options; + commandlog += " "; + commandlog += string_cast<string>(m_o.ops_options, CodePage::UTF8); + } + + commandline += L" -cout+ "; + commandline += m_o.output; + commandlog += " -cout+ "; + commandlog += m_o.output.filename().string(); + + if (!additional_args.empty()) { + commandline += L" "; + commandline += additional_args; + commandlog += " "; + commandlog += string_cast<string>(additional_args, CodePage::UTF8); + } + + if (!preargs.empty()) { + commandline += L" "; + commandline += preargs; + commandlog += " "; + commandlog += string_cast<string>(preargs, CodePage::UTF8); + } + + if (!rel_path.empty()) { + commandline += L" "; + commandline += m_o.mount / rel_path; + commandlog += " "; + commandlog += MOUNT_LABEL + rel_path.string(); + } + + if (!rel_path2.empty()) { + commandline += L" "; + commandline += m_o.mount / rel_path2; + commandlog += " "; + commandlog += MOUNT_LABEL + rel_path2.string(); + } + + if (!postargs.empty()) { + commandline += L" "; + commandline += postargs; + commandlog += " "; + commandlog += string_cast<string>(postargs, CodePage::UTF8); + } + + fprintf(output(), "Spawning: %s\n", commandlog.c_str()); + auto res = usvfs_connector::spawn(&commandline[0]); + fprintf(output(), "\n"); + + bool success = res == 0; + if (success != should_succeed) + throw test::FuncFailed("run_ops", success ? "succeeded" : "failed", + commandlog.c_str(), res); +} + +std::string usvfs_test_base::mount_contents(const path& rel_path) +{ + verify_mount_existance(rel_path); + const auto& contents = test::read_small_file(m_o.mount / rel_path); + return std::string(contents.data(), contents.size()); +} + +std::string usvfs_test_base::source_contents(const path& rel_path) +{ + verify_source_existance(rel_path); + const auto& contents = test::read_small_file(m_o.source / rel_path); + return std::string(contents.data(), contents.size()); +} + +void usvfs_test_base::verify_mount_contents(const path& rel_path, const char* contents) +{ + verify_mount_existance(rel_path); + if (verify_contents(m_o.mount / rel_path, contents)) + throw test::FuncFailed("verify_mount_contents", + (MOUNT_LABEL + rel_path.string()).c_str(), contents); +} + +void usvfs_test_base::verify_source_contents(const path& rel_path, const char* contents) +{ + verify_source_existance(rel_path); + if (verify_contents(m_o.source / rel_path, contents)) + throw test::FuncFailed("verify_source_contents", + (SOURCE_LABEL + rel_path.string()).c_str(), contents); +} + +bool usvfs_test_base::verify_contents(const path& file, const char* contents) +{ + // we allow difference in trailing whitespace (i.e. extra new line): + + size_t sz = strlen(contents); + while (sz && isspace(contents[sz - 1])) + --sz; + + const auto& real_contents = test::read_small_file(file); + size_t real_sz = real_contents.size(); + while (real_sz && isspace(real_contents[real_sz - 1])) + --real_sz; + + return sz == real_sz && memcmp(contents, real_contents.data(), sz); +} + +void usvfs_test_base::verify_mount_existance(const path& rel_path, bool exists, + bool is_dir) +{ + bool real_is_dir = false; + bool real_exists = + winapi::ex::wide::fileExists((m_o.mount / rel_path).c_str(), &real_is_dir); + if (exists != real_exists) + throw test::FuncFailed("verify_mount_existance", + real_exists ? "path exists" : "path does not exist", + (MOUNT_LABEL + rel_path.string()).c_str()); + else if (real_exists && is_dir != real_is_dir) + throw test::FuncFailed("verify_mount_existance", + real_is_dir ? "path is a directory" : "path is a file", + (MOUNT_LABEL + rel_path.string()).c_str()); +} + +void usvfs_test_base::verify_source_existance(const path& rel_path, bool exists, + bool is_dir) +{ + bool real_is_dir = false; + bool real_exists = + winapi::ex::wide::fileExists((m_o.source / rel_path).c_str(), &real_is_dir); + if (exists != real_exists) + throw test::FuncFailed("verify_source_existance", + real_exists ? "path exists" : "path does not exist", + (SOURCE_LABEL + rel_path.string()).c_str()); + else if (real_exists && is_dir != real_is_dir) + throw test::FuncFailed("verify_source_existance", + real_is_dir ? "path is a directory" : "path is a file", + (SOURCE_LABEL + rel_path.string()).c_str()); +} diff --git a/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test_base.h b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test_base.h new file mode 100644 index 0000000..4986e3b --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test_base.h @@ -0,0 +1,120 @@ +#pragma once + +#include <string> + +#include <test_helpers.h> + +class usvfs_test_options +{ +public: + static constexpr auto DEFAULT_MAPPING = L"vfs_mappings.txt"; + static constexpr auto MOUNT_DIR = L"mount"; + static constexpr auto SOURCE_DIR = L"source"; + + using path = test::path; + + // fills any values not set (or set to an empty value) to their default value + void fill_defaults(const path& test_name, const std::wstring& scenario, + const wchar_t* label); + + void set_ops32(); // sets opsexe iff opsexe is empty + void set_ops64(); // sets opsexe iff opsexe is empty + void add_ops_options(const std::wstring& options); + + path opsexe; + path fixture; + path mapping; + path temp; + path mount; + path source; + path output; + path usvfs_log; + std::wstring ops_options; + bool temp_cleanup = false; + bool force_temp_cleanup = false; +}; + +class usvfs_test_base +{ +public: + static constexpr auto MOUNT_DIR = usvfs_test_options::MOUNT_DIR; + static constexpr auto SOURCE_DIR = usvfs_test_options::SOURCE_DIR; + static constexpr auto MOUNT_LABEL = "mount:"; + static constexpr auto SOURCE_LABEL = "source:"; + static constexpr auto OUTPUT_CLEAN_SUFFIX = L"_clean"; + static constexpr auto POSTMORTEM_SUFFIX = L".postmortem"; + + using wstring = std::wstring; + using path = test::path; + + // options object should outlive this object. + usvfs_test_base(const usvfs_test_options& options) : m_o(options) {} + virtual ~usvfs_test_base() = default; + + int run(const std::wstring& exe_name); + + // function for override: + + virtual const char* scenario_name() = 0; + virtual bool scenario_run() = 0; + + // helpers for derived scenarios: + + virtual void ops_list(const path& rel_path, bool recursive, bool with_contents, + bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_read(const path& rel_path, bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_rewrite(const path& rel_path, const char* contents, + bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_overwrite(const path& rel_path, const char* contents, bool recursive, + bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_touch(const path& rel_path, bool full_write_access = false, + bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_deleteoverwrite(const path& rel_path, const char* contents, + bool recursive, bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_delete(const path& rel_path, bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_copy(const path& src_rel_path, const path& dest_rel_path, + bool replace, bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_rename(const path& src_rel_path, const path& dest_rel_path, + bool replace, bool allow_copy = false, + bool should_succeed = true, + const wstring& additional_args = wstring()); + virtual void ops_deleterename(const path& src_rel_path, const path& dest_rel_path, + bool allow_copy = false, bool should_succeed = true, + const wstring& additional_args = wstring()); + + virtual std::string mount_contents(const path& rel_path); + virtual void verify_mount_contents(const path& rel_path, const char* contents); + virtual void verify_mount_existance(const path& rel_path, bool exists = true, + bool is_dir = false); + virtual std::string source_contents(const path& rel_path); + virtual void verify_source_contents(const path& rel_path, const char* contents); + virtual void verify_source_existance(const path& rel_path, bool exists = true, + bool is_dir = false); + +private: + int run_impl(const std::wstring& exe_name); + void log_settings(const std::wstring& exe_name); + void cleanup_temp(); + void copy_fixture(); + bool postmortem_check(); + bool recursive_compare_dirs(path rel_path, path gold_base, path result_base, + FILE* log); + void clean_output(); + + test::ScopedFILE output(); + void run_ops(bool should_succeed, const wstring& preargs, const path& rel_path, + const wstring& additional_args, const wstring& postargs = wstring(), + const path& rel_path2 = path()); + bool verify_contents(const path& file, const char* contents); + + const usvfs_test_options& m_o; + bool m_clean_output = true; +}; diff --git a/libs/usvfs/test/usvfs_test_runner/usvfs_test_runner.cpp b/libs/usvfs/test/usvfs_test_runner/usvfs_test_runner.cpp new file mode 100644 index 0000000..a70fb0f --- /dev/null +++ b/libs/usvfs/test/usvfs_test_runner/usvfs_test_runner.cpp @@ -0,0 +1,105 @@ +#include <gtest/gtest.h> + +#include <boost/filesystem.hpp> +#include <iostream> +#include <test_helpers.h> +#include <windows_sane.h> + +static std::string usvfs_test_command(const char* scenario, const char* platform, + const char* testflag = nullptr, + const char* opsarg = nullptr) +{ + using namespace test; + std::string command = + path_of_test_bin(platform_dependant_executable("usvfs_test", "exe", platform)) + .string(); + if (testflag) { + command += " -"; + command += testflag; + } + if (opsarg) { + command += " -opsarg -"; + command += opsarg; + } + command += " "; + command += scenario; + if (testflag || opsarg) { + command += ":"; + if (testflag) { + command += testflag; + command += "_"; + } + if (opsarg) { + command += opsarg; + command += "_"; + } + command += platform; + } + return command; +} + +static DWORD spawn(std::string commandline) +{ + STARTUPINFOA si{0}; + si.cb = sizeof(si); + PROCESS_INFORMATION pi{0}; + + std::cout << "Running: [" << commandline << "]" << std::endl; + if (!CreateProcessA(NULL, commandline.data(), NULL, NULL, FALSE, 0, NULL, NULL, &si, + &pi)) { + DWORD gle = GetLastError(); + std::cerr << "CreateProcess failed error=" << gle << std::endl; + return 98; + } + + WaitForSingleObject(pi.hProcess, INFINITE); + + DWORD exit = 99; + if (!GetExitCodeProcess(pi.hProcess, &exit)) { + DWORD gle = GetLastError(); + std::cerr << "GetExitCodeProcess failed error=" << gle << std::endl; + } + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + return exit; +} + +TEST(UsvfsTest, basic_x64) +{ + EXPECT_EQ(0, spawn(usvfs_test_command("basic", "x64"))); +} + +TEST(UsvfsTest, basic_x86) +{ + EXPECT_EQ(0, spawn(usvfs_test_command("basic", "x86"))); +} + +TEST(UsvfsTest, basic_ops32_x64) +{ + EXPECT_EQ(0, spawn(usvfs_test_command("basic", "x64", "ops32"))); +} + +TEST(UsvfsTest, basic_ops64_x86) +{ + EXPECT_EQ(0, spawn(usvfs_test_command("basic", "x86", "ops64"))); +} + +/* +TEST(UsvfsTest, basic_ntapi_x64) +{ + EXPECT_EQ(0, spawn(usvfs_test_command("basic", "x64", nullptr, "ntapi"))); +} + +TEST(UsvfsTest, basic_ntapi_x86) +{ + EXPECT_EQ(0, spawn(usvfs_test_command("basic", "x86", nullptr, "ntapi"))); +} +*/ + +int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} |
