dolphin/Externals/WIL/tests/common.h

521 lines
16 KiB
C++

#pragma once
#include <windows.h>
#include <PathCch.h>
#include "catch.hpp"
#include <wil/filesystem.h>
#include <wil/result.h>
#define REPORTS_ERROR(expr) witest::ReportsError(wistd::is_same<HRESULT, decltype(expr)>{}, [&]() { return expr; })
#define REQUIRE_ERROR(expr) REQUIRE(REPORTS_ERROR(expr))
#define REQUIRE_NOERROR(expr) REQUIRE_FALSE(REPORTS_ERROR(expr))
#define CRASHES(expr) witest::DoesCodeCrash([&]() { return expr; })
#define REQUIRE_CRASH(expr) REQUIRE(CRASHES(expr))
#define REQUIRE_NOCRASH(expr) REQUIRE_FALSE(CRASHES(expr))
// NOTE: SUCCEEDED/FAILED macros not used here since Catch2 can give us better diagnostics if it knows the HRESULT value
#define REQUIRE_SUCCEEDED(expr) REQUIRE((HRESULT)(expr) >= 0)
#define REQUIRE_FAILED(expr) REQUIRE((HRESULT)(expr) < 0)
// MACRO double evaluation check.
// The following macro illustrates a common problem with writing macros:
// #define MY_MAX(a, b) (((a) > (b)) ? (a) : (b))
// The issue is that whatever code is being used for both a and b is being executed twice.
// This isn't harmful when thinking of constant numerics, but consider this example:
// MY_MAX(4, InterlockedIncrement(&cCount))
// This evaluates the (B) parameter twice and results in incrementing the counter twice.
// We use MDEC in unit tests to verify that this kind of pattern is not present. A test
// of this kind:
// MY_MAX(MDEC(4), MDEC(InterlockedIncrement(&cCount))
// will verify that the parameters are not evaluated more than once.
#define MDEC(PARAM) (witest::details::MacroDoubleEvaluationCheck(__LINE__, #PARAM), PARAM)
// There's some functionality that we need for testing that's not available for the app partition. Since those tests are
// primarily compilation tests, declare what's needed here
extern "C" {
#if !WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP | WINAPI_PARTITION_SYSTEM | WINAPI_PARTITION_GAMES)
WINBASEAPI _Ret_maybenull_
PVOID WINAPI AddVectoredExceptionHandler(_In_ ULONG First, _In_ PVECTORED_EXCEPTION_HANDLER Handler);
WINBASEAPI
ULONG WINAPI RemoveVectoredExceptionHandler(_In_ PVOID Handle);
#endif
}
#pragma warning(push)
#pragma warning(disable: 4702) // Unreachable code
namespace witest
{
namespace details
{
inline void MacroDoubleEvaluationCheck(size_t uLine, _In_ const char* pszCode)
{
struct SEval
{
size_t uLine;
const char* pszCode;
};
static SEval rgEval[15] = {};
static size_t nOffset = 0;
for (auto& eval : rgEval)
{
if ((eval.uLine == uLine) && (eval.pszCode != nullptr) && (0 == strcmp(pszCode, eval.pszCode)))
{
// This verification indicates that macro-double-evaluation check is firing for a particular usage of MDEC().
FAIL("Expression '" << pszCode << "' double evaluated in macro on line " << uLine);
}
}
rgEval[nOffset].uLine = uLine;
rgEval[nOffset].pszCode = pszCode;
nOffset = (nOffset + 1) % ARRAYSIZE(rgEval);
}
template <typename T>
class AssignTemporaryValueCleanup
{
public:
AssignTemporaryValueCleanup(_In_ AssignTemporaryValueCleanup const &) = delete;
AssignTemporaryValueCleanup & operator=(_In_ AssignTemporaryValueCleanup const &) = delete;
explicit AssignTemporaryValueCleanup(_Inout_ T *pVal, T val) WI_NOEXCEPT :
m_pVal(pVal),
m_valOld(*pVal)
{
*pVal = val;
}
AssignTemporaryValueCleanup(_Inout_ AssignTemporaryValueCleanup && other) WI_NOEXCEPT :
m_pVal(other.m_pVal),
m_valOld(other.m_valOld)
{
other.m_pVal = nullptr;
}
~AssignTemporaryValueCleanup() WI_NOEXCEPT
{
operator()();
}
void operator()() WI_NOEXCEPT
{
if (m_pVal != nullptr)
{
*m_pVal = m_valOld;
m_pVal = nullptr;
}
}
void Dismiss() WI_NOEXCEPT
{
m_pVal = nullptr;
}
private:
T *m_pVal;
T m_valOld;
};
}
// Use the following routine to allow for a variable to be swapped with another and automatically revert the
// assignment at the end of the scope.
// Example:
// int nFoo = 10
// {
// auto revert = witest::AssignTemporaryValue(&nFoo, 12);
// // nFoo will now be 12 within this scope...
// }
// // and nFoo is back to 10 within the outer scope
template <typename T>
inline witest::details::AssignTemporaryValueCleanup<T> AssignTemporaryValue(_Inout_ T *pVal, T val) WI_NOEXCEPT
{
return witest::details::AssignTemporaryValueCleanup<T>(pVal, val);
}
//! Global class which tracks objects that derive from @ref AllocatedObject.
//! Use `witest::g_objectCount.Leaked()` to determine if an object deriving from `AllocatedObject` has been leaked.
class GlobalCount
{
public:
int m_count = 0;
//! Returns `true` if there are any objects that derive from @ref AllocatedObject still in memory.
bool Leaked() const
{
return (m_count != 0);
}
~GlobalCount()
{
if (Leaked())
{
// NOTE: This runs when no test is active, but will still cause an assert failure to notify
FAIL("GlobalCount is non-zero; there is a leak somewhere");
}
}
};
__declspec(selectany) GlobalCount g_objectCount;
//! Derive an allocated test object from witest::AllocatedObject to ensure that those objects aren't leaked in the test.
//! Note that you can call g_objectCount.Leaked() at any point to determine if a leak has already occurred (assuming that
//! all objects should have been destroyed at that point.
class AllocatedObject
{
public:
AllocatedObject() { g_objectCount.m_count++; }
~AllocatedObject() { g_objectCount.m_count--; }
};
template <typename Lambda>
bool DoesCodeThrow(Lambda&& callOp)
{
#ifdef WIL_ENABLE_EXCEPTIONS
try
#endif
{
callOp();
}
#ifdef WIL_ENABLE_EXCEPTIONS
catch (...)
{
return true;
}
#endif
return false;
}
[[noreturn]]
inline void __stdcall TranslateFailFastException(PEXCEPTION_RECORD rec, PCONTEXT, DWORD)
{
// RaiseFailFastException cannot be continued or handled. By instead calling RaiseException, it allows us to
// handle exceptions
::RaiseException(rec->ExceptionCode, rec->ExceptionFlags, rec->NumberParameters, rec->ExceptionInformation);
#ifdef __clang__
__builtin_unreachable();
#endif
}
[[noreturn]]
inline void __stdcall FakeFailfastWithContext(const wil::FailureInfo&) noexcept
{
::RaiseException(STATUS_STACK_BUFFER_OVERRUN, 0, 0, nullptr);
#ifdef __clang__
__builtin_unreachable();
#endif
}
constexpr DWORD msvc_exception_code = 0xE06D7363;
// This is a MAJOR hack. Catch2 registers a vectored exception handler - which gets run before our handler below -
// that interprets a set of exception codes as fatal. We don't want this behavior since we may be expecting such
// crashes, so instead translate all exception codes to something not fatal
inline LONG WINAPI TranslateExceptionCodeHandler(PEXCEPTION_POINTERS info)
{
if (info->ExceptionRecord->ExceptionCode != witest::msvc_exception_code)
{
info->ExceptionRecord->ExceptionCode = STATUS_STACK_BUFFER_OVERRUN;
}
return EXCEPTION_CONTINUE_SEARCH;
}
namespace details
{
inline bool DoesCodeCrash(wistd::function<void()>& callOp)
{
bool result = false;
__try
{
callOp();
}
// Let C++ exceptions pass through
__except ((::GetExceptionCode() != msvc_exception_code) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
{
result = true;
}
return result;
}
}
inline bool DoesCodeCrash(wistd::function<void()> callOp)
{
// See above; we don't want to actually fail fast, so make sure we raise a different exception instead
auto restoreHandler = AssignTemporaryValue(&wil::details::g_pfnRaiseFailFastException, TranslateFailFastException);
auto restoreHandler2 = AssignTemporaryValue(&wil::details::g_pfnFailfastWithContextCallback, FakeFailfastWithContext);
auto handler = AddVectoredExceptionHandler(1, TranslateExceptionCodeHandler);
auto removeVectoredHandler = wil::scope_exit([&] { RemoveVectoredExceptionHandler(handler); });
return details::DoesCodeCrash(callOp);
}
template <typename Lambda>
bool ReportsError(wistd::false_type, Lambda&& callOp)
{
bool doesThrow = false;
bool doesCrash = DoesCodeCrash([&]()
{
doesThrow = DoesCodeThrow(callOp);
});
return doesThrow || doesCrash;
}
template <typename Lambda>
bool ReportsError(wistd::true_type, Lambda&& callOp)
{
return FAILED(callOp());
}
#ifdef WIL_ENABLE_EXCEPTIONS
class TestFailureCache final :
public wil::details::IFailureCallback
{
public:
TestFailureCache() :
m_callbackHolder(this)
{
}
void clear()
{
m_failures.clear();
}
size_t size() const
{
return m_failures.size();
}
bool empty() const
{
return m_failures.empty();
}
const wil::FailureInfo& operator[](size_t pos) const
{
return m_failures.at(pos).GetFailureInfo();
}
// IFailureCallback
bool NotifyFailure(wil::FailureInfo const & failure) WI_NOEXCEPT override
{
m_failures.emplace_back(failure);
return false;
}
private:
std::vector<wil::StoredFailureInfo> m_failures;
wil::details::ThreadFailureCallbackHolder m_callbackHolder;
};
#endif
inline HRESULT GetTempFileName(wchar_t (&result)[MAX_PATH])
{
wchar_t dir[MAX_PATH];
RETURN_LAST_ERROR_IF(::GetTempPathW(MAX_PATH, dir) == 0);
RETURN_LAST_ERROR_IF(::GetTempFileNameW(dir, L"wil", 0, result) == 0);
return S_OK;
}
inline HRESULT CreateUniqueFolderPath(wchar_t (&buffer)[MAX_PATH], PCWSTR root = nullptr)
{
if (root)
{
RETURN_LAST_ERROR_IF(::GetTempFileNameW(root, L"wil", 0, buffer) == 0);
}
else
{
wchar_t tempPath[MAX_PATH];
RETURN_LAST_ERROR_IF(::GetTempPathW(ARRAYSIZE(tempPath), tempPath) == 0);
RETURN_LAST_ERROR_IF(::GetLongPathNameW(tempPath, tempPath, ARRAYSIZE(tempPath)) == 0);
RETURN_LAST_ERROR_IF(::GetTempFileNameW(tempPath, L"wil", 0, buffer) == 0);
}
RETURN_IF_WIN32_BOOL_FALSE(DeleteFileW(buffer));
PathCchRemoveExtension(buffer, ARRAYSIZE(buffer));
return S_OK;
}
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP) && (_WIN32_WINNT >= _WIN32_WINNT_WIN7)
struct TestFolder
{
TestFolder()
{
if (SUCCEEDED(CreateUniqueFolderPath(m_path)) && SUCCEEDED(wil::CreateDirectoryDeepNoThrow(m_path)))
{
m_valid = true;
}
}
TestFolder(PCWSTR path)
{
if (SUCCEEDED(StringCchCopyW(m_path, ARRAYSIZE(m_path), path)) && SUCCEEDED(wil::CreateDirectoryDeepNoThrow(m_path)))
{
m_valid = true;
}
}
TestFolder(const TestFolder&) = delete;
TestFolder& operator=(const TestFolder&) = delete;
TestFolder(TestFolder&& other)
{
if (other.m_valid)
{
m_valid = true;
other.m_valid = false;
wcscpy_s(m_path, other.m_path);
}
}
~TestFolder()
{
if (m_valid)
{
wil::RemoveDirectoryRecursiveNoThrow(m_path);
}
}
operator bool() const
{
return m_valid;
}
operator PCWSTR() const
{
return m_path;
}
PCWSTR Path() const
{
return m_path;
}
private:
bool m_valid = false;
wchar_t m_path[MAX_PATH] = L"";
};
struct TestFile
{
TestFile(PCWSTR path)
{
if (SUCCEEDED(StringCchCopyW(m_path, ARRAYSIZE(m_path), path)))
{
Create();
}
}
TestFile(PCWSTR dirPath, PCWSTR fileName)
{
if (SUCCEEDED(StringCchCopyW(m_path, ARRAYSIZE(m_path), dirPath)) && SUCCEEDED(PathCchAppend(m_path, ARRAYSIZE(m_path), fileName)))
{
Create();
}
}
TestFile(const TestFile&) = delete;
TestFile& operator=(const TestFile&) = delete;
TestFile(TestFile&& other)
{
if (other.m_valid)
{
m_valid = true;
m_deleteDir = other.m_deleteDir;
other.m_valid = other.m_deleteDir = false;
wcscpy_s(m_path, other.m_path);
}
}
~TestFile()
{
// Best effort on all of these
if (m_valid)
{
::DeleteFileW(m_path);
}
if (m_deleteDir)
{
size_t parentLength;
if (wil::try_get_parent_path_range(m_path, &parentLength))
{
m_path[parentLength] = L'\0';
::RemoveDirectoryW(m_path);
m_path[parentLength] = L'\\';
}
}
}
operator bool() const
{
return m_valid;
}
operator PCWSTR() const
{
return m_path;
}
PCWSTR Path() const
{
return m_path;
}
private:
HRESULT Create()
{
WI_ASSERT(!m_valid && !m_deleteDir);
wil::unique_hfile fileHandle(::CreateFileW(m_path,
FILE_WRITE_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));
if (!fileHandle)
{
auto err = ::GetLastError();
size_t parentLength;
if ((err == ERROR_PATH_NOT_FOUND) && wil::try_get_parent_path_range(m_path, &parentLength))
{
m_path[parentLength] = L'\0';
RETURN_IF_FAILED(wil::CreateDirectoryDeepNoThrow(m_path));
m_deleteDir = true;
m_path[parentLength] = L'\\';
fileHandle.reset(::CreateFileW(m_path,
FILE_WRITE_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));
RETURN_LAST_ERROR_IF(!fileHandle);
}
else
{
RETURN_WIN32(err);
}
}
m_valid = true;
return S_OK;
}
bool m_valid = false;
bool m_deleteDir = false;
wchar_t m_path[MAX_PATH] = L"";
};
#endif
}
#pragma warning(pop)