Skip to content

Commit

Permalink
Clean code of USVFS global test.
Browse files Browse the repository at this point in the history
  • Loading branch information
Holt59 committed Jun 22, 2024
1 parent 42f469c commit e3c1679
Show file tree
Hide file tree
Showing 7 changed files with 403 additions and 319 deletions.
71 changes: 71 additions & 0 deletions test/usvfs_global_test_runner/gtest_utils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#pragma once

#include <filesystem>
#include <format>

#include <boost/algorithm/string.hpp>

#include <gtest/gtest.h>

::testing::AssertionResult AssertDirectoryEquals(const std::filesystem::path& expected,
const std::filesystem::path& actual,
bool content = true)
{
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 (!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))
196 changes: 196 additions & 0 deletions test/usvfs_global_test_runner/usvfs_global_test_fixture.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
#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;
}

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::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;
}
77 changes: 77 additions & 0 deletions test/usvfs_global_test_runner/usvfs_global_test_fixture.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#pragma once

#include <filesystem>
#include <memory>

#include <gtest/gtest.h>

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<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";
};
Loading

0 comments on commit e3c1679

Please sign in to comment.