aboutsummaryrefslogtreecommitdiff
path: root/libs/usvfs/test/usvfs_global_test_runner
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/usvfs_global_test_runner
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/usvfs_global_test_runner')
-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
5 files changed, 584 insertions, 0 deletions
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();
+}