aboutsummaryrefslogtreecommitdiff
path: root/libs/usvfs/test
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /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')
-rw-r--r--libs/usvfs/test/CMakeLists.txt74
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/data/file.txt0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/docs/doc.txt0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/empty/.gitkeep0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/expected/mods/mod1/readme.txt0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/data/file.txt0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/docs/doc.txt0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/empty/.gitkeep0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/info.txt0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/BasicTest/source/mods/mod1/readme.txt0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/data/.gitkeep0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/mods/mod1/docs/doc.txt0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/expected/mods/mod1/docs/subdocs/.gitkeep0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/data/.gitkeep0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/mods/mod1/docs/doc.txt0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/BoostFilesystemTest/source/mods/mod1/docs/subdocs/.gitkeep0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/example.cpp139
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/data/.gitkeep0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/mods/HUD Painter/r6/storages/HUDPainter/DEFAULT.json11
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/expected/overwrite/r6/storages/HUDPainter/TEST.json1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/source/data/.gitkeep0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/RedFileSystemTest/source/mods/HUD Painter/r6/storages/HUDPainter/DEFAULT.json11
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/data/docs/doc.skip1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/data/file.txt0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/docs/doc.skip1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/docs/doc.txt0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/empty/.gitkeep0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/readme.skip1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/mods/mod1/readme.txt0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/expected/overwrite/readme.skip1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/data/docs/doc.skip1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/data/file.txt0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/docs/doc.skip1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/docs/doc.txt0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/empty/.gitkeep0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/readme.skip1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_global_test/SkipFilesTest/source/mods/mod1/readme.txt0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rcopyme4.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfile0.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfiledeletewrite.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/rfolder/rfilerewrite.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root0.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root0w.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root1.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount.postmortem/root1w.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rcopyme4.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfile0.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfiledelete.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfiledeletewrite.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfileoldname.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount/rfolder/rfilerewrite.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount/root0.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount/root0w.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount/root1.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/mount/root1w.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder1/mfile.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder1/mfilew.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mfolder3/mdeep3/mfile.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mod1.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod1/mod1w.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder2/mfile.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfile.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletemove.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletemove2p.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletewrite.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfiledeletewrite2p.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfilemoveover.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfileoverwrite.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mfolder4/mfilerewrite.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod2/mod2.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mfolder3/mdeep3/mfile.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mfolder4/mfile.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod3/mod3.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfile.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfiledeletemove.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfiledeletewrite.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfilemoveover.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfileoverwrite.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/mod4/mfolder4/mfilerewrite.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/.empty0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder1/newfile1.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder1/newfolder1/newfile1.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder2/newfile2.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder3/newfolder3/newfile3.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder3/newfolder3/newfile3e.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/mfiledeletemove2p.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/mfiledeletewrite2p.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4e.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/e/p/newfile4enr.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/epnewfile4.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4/d/e/epnewfile4r.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/newfolder4p/rcopyme4.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/mfolder4/rcopyme4.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/overwrite/rfolder/rfilenewname.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root1.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root1w.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root2.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source.postmortem/root2w.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder1/mfile.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder1/mfilew.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mfolder3/mdeep3/mfile.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mod1.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod1/mod1w.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder2/mfile.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfile.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletemove.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletemove2p.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletewrite.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfiledeletewrite2p.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfilemoveover.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfileoverwrite.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mfolder4/mfilerewrite.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod2/mod2.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mfolder3/mdeep3/mfile.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mfolder4/mfile.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod3/mod3.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfile.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletemove.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletemove2p.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletewrite.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfiledeletewrite2p.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfilemoveover.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfileoverwrite.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/mod4/mfolder4/mfilerewrite.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/overwrite/.empty0
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/root1.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/root1w.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/root2.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/source/root2w.txt1
-rw-r--r--libs/usvfs/test/fixtures/usvfs_test/basic/vfs_mappings.txt26
-rw-r--r--libs/usvfs/test/gtest_utils/CMakeLists.txt11
-rw-r--r--libs/usvfs/test/gtest_utils/gtest_utils.cpp122
-rw-r--r--libs/usvfs/test/gtest_utils/gtest_utils.h29
-rw-r--r--libs/usvfs/test/shared_test/CMakeLists.txt7
-rw-r--r--libs/usvfs/test/shared_test/main.cpp248
-rw-r--r--libs/usvfs/test/test_utils/CMakeLists.txt11
-rw-r--r--libs/usvfs/test/test_utils/test_helpers.cpp248
-rw-r--r--libs/usvfs/test/test_utils/test_helpers.h165
-rw-r--r--libs/usvfs/test/thooklib_test/CMakeLists.txt11
-rw-r--r--libs/usvfs/test/thooklib_test/main.cpp235
-rw-r--r--libs/usvfs/test/thooklib_test/test_hooks.cpp38
-rw-r--r--libs/usvfs/test/tinjectlib_test/CMakeLists.txt16
-rw-r--r--libs/usvfs/test/tinjectlib_test/main.cpp158
-rw-r--r--libs/usvfs/test/tinjectlib_test/testinject_bin/main.cpp4
-rw-r--r--libs/usvfs/test/tinjectlib_test/testinject_dll/main.cpp36
-rw-r--r--libs/usvfs/test/tinjectlib_test/testinject_dll/main.h11
-rw-r--r--libs/usvfs/test/tvfs_test/CMakeLists.txt12
-rw-r--r--libs/usvfs/test/tvfs_test/main.cpp585
-rw-r--r--libs/usvfs/test/usvfs_global_test_runner/CMakeLists.txt23
-rw-r--r--libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test/usvfs_global_test.cpp216
-rw-r--r--libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_fixture.cpp205
-rw-r--r--libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_fixture.h82
-rw-r--r--libs/usvfs/test/usvfs_global_test_runner/usvfs_global_test_runner.cpp58
-rw-r--r--libs/usvfs/test/usvfs_test_runner/CMakeLists.txt35
-rw-r--r--libs/usvfs/test/usvfs_test_runner/test_file_operations/test_file_operations.cpp496
-rw-r--r--libs/usvfs/test/usvfs_test_runner/test_file_operations/test_filesystem.cpp159
-rw-r--r--libs/usvfs/test/usvfs_test_runner/test_file_operations/test_filesystem.h104
-rw-r--r--libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntapi.cpp488
-rw-r--r--libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntapi.h38
-rw-r--r--libs/usvfs/test/usvfs_test_runner/test_file_operations/test_ntdll_declarations.h157
-rw-r--r--libs/usvfs/test/usvfs_test_runner/test_file_operations/test_w32api.cpp370
-rw-r--r--libs/usvfs/test/usvfs_test_runner/test_file_operations/test_w32api.h36
-rw-r--r--libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.cpp402
-rw-r--r--libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_basic_test.h14
-rw-r--r--libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test.cpp242
-rw-r--r--libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test_base.cpp1002
-rw-r--r--libs/usvfs/test/usvfs_test_runner/usvfs_test/usvfs_test_base.h120
-rw-r--r--libs/usvfs/test/usvfs_test_runner/usvfs_test_runner.cpp105
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(&params, "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(&params, "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();
+}