From e3c167999fc65d16a835a2468ff7a39a17e9e679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Sun, 16 Jun 2024 15:57:36 +0200 Subject: [PATCH] Clean code of USVFS global test. --- test/usvfs_global_test_runner/gtest_utils.h | 71 ++++ .../usvfs_global_test_fixture.cpp | 196 +++++++++++ .../usvfs_global_test_fixture.h | 77 ++++ .../usvfs_global_test_runner.cpp | 332 +----------------- vsbuild/usvfs_global_test.vcxproj.filters | 14 + vsbuild/usvfs_global_test_runner.vcxproj | 5 + .../usvfs_global_test_runner.vcxproj.filters | 27 ++ 7 files changed, 403 insertions(+), 319 deletions(-) create mode 100644 test/usvfs_global_test_runner/gtest_utils.h create mode 100644 test/usvfs_global_test_runner/usvfs_global_test_fixture.cpp create mode 100644 test/usvfs_global_test_runner/usvfs_global_test_fixture.h create mode 100644 vsbuild/usvfs_global_test.vcxproj.filters create mode 100644 vsbuild/usvfs_global_test_runner.vcxproj.filters diff --git a/test/usvfs_global_test_runner/gtest_utils.h b/test/usvfs_global_test_runner/gtest_utils.h new file mode 100644 index 0000000..b604568 --- /dev/null +++ b/test/usvfs_global_test_runner/gtest_utils.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include + +#include + +#include + +::testing::AssertionResult AssertDirectoryEquals(const std::filesystem::path& expected, + const std::filesystem::path& actual, + bool content = true) +{ + std::vector failure_messages; + std::vector 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 (!test::compare_files(expected_path, actual_path, true)) { + 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"; +} + +#define ASSERT_DIRECTORY_EQ(Expected, Actual) \ + ASSERT_TRUE(AssertDirectoryEquals(Expected, Actual)) \ No newline at end of file diff --git a/test/usvfs_global_test_runner/usvfs_global_test_fixture.cpp b/test/usvfs_global_test_runner/usvfs_global_test_fixture.cpp new file mode 100644 index 0000000..59ddd3d --- /dev/null +++ b/test/usvfs_global_test_runner/usvfs_global_test_fixture.cpp @@ -0,0 +1,196 @@ +#include "usvfs_global_test_fixture.h" + +#include +#include +#include + +#include + +#include +#include +#include +#include + +// 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& arguments = {}, + const std::optional& working_directory = {}) +{ + using namespace usvfs::shared; + + std::wstring command = app; + std::filesystem::path cwd = working_directory.value_or(app.parent_path()); + std::vector 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(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; +} + +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 m_parameters; +}; + +UsvfsGlobalTest::UsvfsGlobalTest() + : m_usvfs{std::make_unique()}, + 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::PreapreMapping() 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 +{ + 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 (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/test/usvfs_global_test_runner/usvfs_global_test_fixture.h b/test/usvfs_global_test_runner/usvfs_global_test_fixture.h new file mode 100644 index 0000000..1a9704d --- /dev/null +++ b/test/usvfs_global_test_runner/usvfs_global_test_fixture.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include + +#include + +class UsvfsGlobalTest : public testing::Test +{ +public: + UsvfsGlobalTest(); + + void SetUp() override + { + PrepareFileSystem(); + PreapreMapping(); + } + + 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; + + // 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 PreapreMapping() const; + + // cleanup the temporary path + // + void CleanUp() const; + + // usvfs_guard + std::unique_ptr 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"; +}; \ No newline at end of file diff --git a/test/usvfs_global_test_runner/usvfs_global_test_runner.cpp b/test/usvfs_global_test_runner/usvfs_global_test_runner.cpp index 2cc2bd0..4c49a01 100644 --- a/test/usvfs_global_test_runner/usvfs_global_test_runner.cpp +++ b/test/usvfs_global_test_runner/usvfs_global_test_runner.cpp @@ -1,336 +1,30 @@ -#include -#include -#include -#include - -#include - #include -#include "stringcast.h" -#include "test_helpers.h" -#include "usvfs.h" -#include "windows_sane.h" - -// find the path to the USVFS DLL that contains gtest entries -// -std::filesystem::path path_to_usvfs_dll() -{ - return test::path_of_usvfs_lib(test::platform_dependant_executable("usvfs", "dll")); -} - -// 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& arguments = {}, - const std::optional& working_directory = {}) -{ - using namespace usvfs::shared; - - std::wstring command = app; - std::filesystem::path cwd = working_directory.value_or(app.parent_path()); - std::vector 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(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; -} - -struct comparison_result -{ - std::vector left_only; - std::vector right_only; - std::vector type_differ; - std::vector content_differ; - - bool empty() const - { - return left_only.empty() && right_only.empty() && type_differ.empty() && - content_differ.empty(); - } -}; - -comparison_result compare_directories(const std::filesystem::path& left, - const std::filesystem::path& right, - bool content = true) -{ - comparison_result result; - std::vector in_both; - - // iterate left, check on right - for (const auto& it : std::filesystem::recursive_directory_iterator{left}) { - const auto relpath = relative(it.path(), left); - if (!exists(right / relpath)) { - result.left_only.push_back(relpath); - } else { - in_both.push_back(relpath); - } - } - - // iterate right, check on left - for (const auto& it : std::filesystem::recursive_directory_iterator{right}) { - const auto relpath = relative(it.path(), right); - if (!exists(left / relpath)) { - result.right_only.push_back(relpath); - } - } - - // check contents - if (content) { - for (const auto& relpath : in_both) { - const auto left_path = left / relpath, right_path = right / relpath; - - if (is_directory(left_path) != is_directory(right_path)) { - result.type_differ.push_back(relpath); - continue; - } - - if (is_directory(left_path)) { - continue; - } - - if (!test::compare_files(left_path, right_path, true)) { - result.content_differ.push_back(relpath); - } - } - } - - return result; -} - -class usvfs_guard -{ -public: - usvfs_guard(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()); - } - - ~usvfs_guard() { usvfsDisconnectVFS(); } - -private: - std::unique_ptr m_parameters; -}; - -class usvfs_test_runner -{ -public: - usvfs_test_runner(std::wstring_view test_group, bool overwrite = true) - : m_group{test_group}, m_temporary_folder{test::path_of_test_temp()}, - m_overwrite{overwrite} - {} - ~usvfs_test_runner() { cleanup(); } - - // run the test, return the exit code of the google test process - // - int run() const; - -private: - // 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 prepare_filesystem() const; - - // prepare mapping using the given set of paths - // - void prepare_mapping(const std::filesystem::path& data, - const std::filesystem::path& mods, - const std::filesystem::path& overwrite) const; - - // cleanup the temporary path - // - void cleanup() const; - - // 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; - - // use an overwrite folder or not - bool m_overwrite; -}; - -void usvfs_test_runner::cleanup() const -{ - if (exists(m_temporary_folder)) { - remove_all(m_temporary_folder); - } -} - -void usvfs_test_runner::prepare_filesystem() 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); - } - - const auto overwrite = m_temporary_folder / L"overwrite"; - if (m_overwrite && !exists(overwrite)) { - create_directory(overwrite); - } -} - -void usvfs_test_runner::prepare_mapping(const std::filesystem::path& data, - const std::filesystem::path& mods, - const std::filesystem::path& overwrite) const -{ - // should not be needed, but just to be safe - usvfsClearVirtualMappings(); - - if (!exists(data)) { - throw std::runtime_error{std::format("data path missing at {}", data.string())}; - } - - if (exists(mods)) { - for (const auto& mod : std::filesystem::directory_iterator(mods)) { - if (!is_directory(mod)) { - continue; - } - usvfsVirtualLinkDirectoryStatic(mod.path().c_str(), data.c_str(), - LINKFLAG_RECURSIVE); - } - } - - if (exists(overwrite)) { - usvfsVirtualLinkDirectoryStatic(overwrite.c_str(), data.c_str(), - LINKFLAG_CREATETARGET | LINKFLAG_RECURSIVE); - } -} - -int usvfs_test_runner::run() const -{ - // copy files from fixtures to a relevant location - prepare_filesystem(); - - // data folder - const auto data = m_temporary_folder / L"data"; - - // prepare usvfs and mapping - usvfs_guard guard{"usvfs_test", true}; - - prepare_mapping(data, m_temporary_folder / L"mods", - m_temporary_folder / L"overwrite"); - - const auto res = spawn_usvfs_hooked_process( - path_to_usvfs_global_test(), - {std::format(L"--gtest_filter={}.*", m_group), data.native()}); +#include +#include - if (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; - } - - // check contents of directories - auto diff = - compare_directories(path_to_usvfs_global_test_figures(m_group) / "expected", - m_temporary_folder, true); - - if (!diff.empty()) { - std::cout << "expected and actual folder content differs\n"; - std::cout << " expected but not found:\n"; - for (auto& path : diff.left_only) { - std::cout << " " << path.string() << "\n"; - } - std::cout << " not expected but found:\n"; - for (auto& path : diff.right_only) { - std::cout << " " << path.string() << "\n"; - } - std::cout << " content differ:\n"; - for (auto& path : diff.content_differ) { - std::cout << " " << path.string() << "\n"; - } - return -1; - } - - return res; -} +#include "gtest_utils.h" +#include "usvfs_global_test_fixture.h" -TEST(BasicTest, Test) +TEST_F(UsvfsGlobalTest, BasicTest) { - ASSERT_EQ(0, usvfs_test_runner(L"BasicTest", false).run()); + ASSERT_EQ(0, Run()); + ASSERT_DIRECTORY_EQ(ExpectedFolder(), ActualFolder()); } -TEST(RedFileSystemTest, Test) +TEST_F(UsvfsGlobalTest, RedFileSystemTest) { - ASSERT_EQ(0, usvfs_test_runner(L"RedFileSystemTest").run()); + SetUpOverwrite(true); + ASSERT_EQ(0, Run()); + ASSERT_DIRECTORY_EQ(ExpectedFolder(), ActualFolder()); } int main(int argc, char* argv[]) { // load the USVFS DLL // - const auto usvfs_dll = path_to_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", " diff --git a/vsbuild/usvfs_global_test.vcxproj.filters b/vsbuild/usvfs_global_test.vcxproj.filters new file mode 100644 index 0000000..b4e8d7c --- /dev/null +++ b/vsbuild/usvfs_global_test.vcxproj.filters @@ -0,0 +1,14 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx + + + + + src + + + \ No newline at end of file diff --git a/vsbuild/usvfs_global_test_runner.vcxproj b/vsbuild/usvfs_global_test_runner.vcxproj index 4e119a4..4e8a74e 100644 --- a/vsbuild/usvfs_global_test_runner.vcxproj +++ b/vsbuild/usvfs_global_test_runner.vcxproj @@ -19,6 +19,7 @@ + @@ -104,6 +105,10 @@ {4a355317-3634-4c98-afb1-8ba718d54727} + + + + diff --git a/vsbuild/usvfs_global_test_runner.vcxproj.filters b/vsbuild/usvfs_global_test_runner.vcxproj.filters new file mode 100644 index 0000000..a276971 --- /dev/null +++ b/vsbuild/usvfs_global_test_runner.vcxproj.filters @@ -0,0 +1,27 @@ + + + + + {87a9ca7b-9f75-4770-9cab-e7e1d149c76b} + + + {ca9cea9d-9155-45c4-a9bf-ed98b3f28105} + + + + + src + + + src + + + + + inc + + + inc + + + \ No newline at end of file