123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521 |
- #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)
|