diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /xpcom/tests/gtest/TestHandleWatcher.cpp | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'xpcom/tests/gtest/TestHandleWatcher.cpp')
-rw-r--r-- | xpcom/tests/gtest/TestHandleWatcher.cpp | 580 |
1 files changed, 580 insertions, 0 deletions
diff --git a/xpcom/tests/gtest/TestHandleWatcher.cpp b/xpcom/tests/gtest/TestHandleWatcher.cpp new file mode 100644 index 0000000000..c003a026a1 --- /dev/null +++ b/xpcom/tests/gtest/TestHandleWatcher.cpp @@ -0,0 +1,580 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "gtest/gtest.h" + +#include <minwindef.h> +#include <handleapi.h> +#include <synchapi.h> + +#include "mozilla/Assertions.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/Result.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/SharedThreadPool.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/TaskQueue.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/WinHandleWatcher.h" +#include "nsCOMPtr.h" +#include "nsError.h" +#include "nsIEventTarget.h" +#include "nsITargetShutdownTask.h" +#include "nsIThread.h" +#include "nsIThreadShutdown.h" +#include "nsITimer.h" +#include "nsTHashMap.h" +#include "nsThreadUtils.h" +// #include "nscore.h" + +namespace details { +static nsCString MakeTargetName(const char* name) { + const char* testName = + ::testing::UnitTest::GetInstance()->current_test_info()->name(); + nsCString ret; + ret.AppendPrintf("%s: %s", testName, name); + return ret; +} +} // namespace details + +using HandleWatcher = mozilla::HandleWatcher; + +/////////////////////////////////////////////////////////////////////// +// Error handling + +// nsresult_fatal_err_: auxiliary function for testing-macros. +[[noreturn]] void nsresult_fatal_err_(const char* file, size_t line, + const char* expr, nsresult res) { + // implementation details from the MOZ_CRASH* family of macros + MOZ_Crash(file, static_cast<int>(line), + MOZ_CrashPrintf("%s gave nsresult %s(%" PRIX32 ")", expr, + mozilla::GetStaticErrorName(res), uint32_t(res))); +} + +// UNWRAP: testing-oriented variant of Result::unwrap. +// +// We make no use of gtest's `ASSERT_*` family of macros, since they assume +// that a `return;` statement is sufficient to abort the test. +template <typename T> +T unwrap_impl_(const char* file, size_t line, const char* expr, + mozilla::Result<T, nsresult> res) { + if (MOZ_LIKELY(res.isOk())) { + return res.unwrap(); + } + nsresult_fatal_err_(file, line, expr, res.unwrapErr()); +} + +#define UNWRAP(expr) unwrap_impl_(__FILE__, __LINE__, #expr, expr) + +/////////////////////////////////////////////////////////////////////// +// Milliseconds() +// +// Convenience declaration for millisecond-based mozilla::TimeDurations. +static mozilla::TimeDuration Milliseconds(double d) { + return mozilla::TimeDuration::FromMilliseconds(d); +} + +/////////////////////////////////////////////////////////////////////// +// TestHandleWatcher +// +// GTest test fixture. Provides shared resources. +class TestHandleWatcher : public testing::Test { + protected: + static void SetUpTestSuite() { sIsLive = true; } + static void TearDownTestSuite() { + sPool = nullptr; + sIsLive = false; + } + + public: + static already_AddRefed<mozilla::SharedThreadPool> GetPool() { + AssertIsLive(); + if (!sPool) { + sPool = mozilla::SharedThreadPool::Get("Test Pool"_ns); + } + return do_AddRef(sPool); + } + + private: + static bool sIsLive; // just for confirmation + static void AssertIsLive() { + MOZ_ASSERT(sIsLive, + "attempted to use `class TestHandleWatcher` outside test group"); + } + + static RefPtr<mozilla::SharedThreadPool> sPool; +}; + +/* static */ +bool TestHandleWatcher::sIsLive = false; +/* static */ +RefPtr<mozilla::SharedThreadPool> TestHandleWatcher::sPool = nullptr; + +/////////////////////////////////////////////////////////////////////// +// WindowsEventObject +// +// Convenient interface to a Windows `event` object. (This is a synchronization +// object that's usually the wrong thing to use.) +struct WindowsEventObject { + HANDLE const handle = ::CreateEvent(nullptr, TRUE, FALSE, nullptr); + WindowsEventObject() = default; + ~WindowsEventObject() { ::CloseHandle(handle); } + + WindowsEventObject(WindowsEventObject const&) = delete; + WindowsEventObject(WindowsEventObject&&) = delete; + WindowsEventObject& operator=(WindowsEventObject const&) = delete; + WindowsEventObject& operator=(WindowsEventObject&&) = delete; + + void Set() { ::SetEvent(handle); } +}; + +/////////////////////////////////////////////////////////////////////// +// SpawnNewThread +// +nsCOMPtr<nsIThread> SpawnNewThread(const char* name) { + nsCOMPtr<nsIThread> thread; + MOZ_ALWAYS_SUCCEEDS( + NS_NewNamedThread(details::MakeTargetName(name), getter_AddRefs(thread))); + return thread; +} + +/////////////////////////////////////////////////////////////////////// +// SpawnNewBackgroundQueue +// +// (mozilla::TaskQueue expects the supplied name to outlive the queue, so we +// just use a static string.) +RefPtr<mozilla::TaskQueue> SpawnNewBackgroundQueue() { + return mozilla::TaskQueue::Create(TestHandleWatcher::GetPool(), + "task queue for TestHandleWatcher"); +} + +/////////////////////////////////////////////////////////////////////// +// SpinEventLoopUntil +// +// Local equivalent of `mozilla::SpinEventLoopUntil`, extended with a timeout. +// +// Spin the current thread's event loop until either a specified predicate is +// satisfied or a specified time-interval has passed. +// +struct SpinEventLoopUntilRet { + enum Value { Ok, TimedOut, InternalError } value; + bool ok() const { return value == Value::Ok; } + bool timedOut() const { return value == Value::TimedOut; } + + MOZ_IMPLICIT SpinEventLoopUntilRet(Value v) : value(v) {} +}; +template <typename Predicate> +SpinEventLoopUntilRet SpinEventLoopUntil( + Predicate const& aPredicate, + mozilla::TimeDuration aDuration = Milliseconds(500)) { + using Value = SpinEventLoopUntilRet::Value; + nsIThread* currentThread = NS_GetCurrentThread(); + + // Set up timer. + bool timedOut = false; + auto timer = UNWRAP(NS_NewTimerWithCallback( + [&](nsITimer*) { timedOut = true; }, aDuration, nsITimer::TYPE_ONE_SHOT, + "SpinEventLoop timer", currentThread)); + auto onExitCancelTimer = mozilla::MakeScopeExit([&] { timer->Cancel(); }); + + bool const ret = mozilla::SpinEventLoopUntil( + "TestHandleWatcher"_ns, [&] { return timedOut || aPredicate(); }); + if (!ret) return Value::InternalError; + if (timedOut) return Value::TimedOut; + return Value::Ok; +} + +// metatest for `SpinEventLoopUntil` +TEST_F(TestHandleWatcher, SpinEventLoopUntil) { + auto should_fail = SpinEventLoopUntil([] { return false; }, Milliseconds(1)); + ASSERT_TRUE(should_fail.timedOut()); + auto should_pass = SpinEventLoopUntil([] { return true; }, Milliseconds(50)); + ASSERT_TRUE(should_pass.ok()); +} + +/////////////////////////////////////////////////////////////////////// +// PingMainThread +// +// Post a do-nothing message to the main thread's event queue. (This will signal +// it to wake up and check its predicate, if it's waiting for that to happen.) +void PingMainThread() { + MOZ_ALWAYS_SUCCEEDS( + NS_DispatchToMainThread(NS_NewRunnableFunction("Ping", [] {}))); +} + +/////////////////////////////////////////////////////////////////////// +// Individual tests + +// Test basic creation and destruction. +TEST_F(TestHandleWatcher, Trivial) { HandleWatcher hw; } + +// Test interaction before a Watch is created. +TEST_F(TestHandleWatcher, Empty) { + HandleWatcher hw; + ASSERT_TRUE(hw.IsStopped()); + hw.Stop(); +} + +// Start and trigger an HandleWatcher directly from the main thread. +TEST_F(TestHandleWatcher, Simple) { + WindowsEventObject event; + HandleWatcher hw; + + std::atomic<bool> run = false; + + hw.Watch( + event.handle, NS_GetCurrentThread(), + NS_NewRunnableFunction("TestHandleWatcher::Simple", [&] { run = true; })); + + ASSERT_FALSE(run.load()); + event.Set(); + // Attempt to force a race below. + ::Sleep(0); + // This should not race. The HandleWatcher doesn't execute its delegate; it + // just queues a mozilla::Task to do that onto our thread's event queue, + // and that Task hasn't been permitted to run yet. + ASSERT_FALSE(run.load()); + + ASSERT_TRUE(SpinEventLoopUntil([&] { return run.load(); }).ok()); +} + +// Test that calling Stop() stops the watcher. +TEST_F(TestHandleWatcher, Stop) { + WindowsEventObject event; + HandleWatcher hw; + std::atomic<bool> run = false; + + hw.Watch( + event.handle, NS_GetCurrentThread(), + NS_NewRunnableFunction("TestHandleWatcher::Stop", [&] { run = true; })); + + ASSERT_FALSE(hw.IsStopped()); + hw.Stop(); + ASSERT_TRUE(hw.IsStopped()); + + ASSERT_TRUE(SpinEventLoopUntil([&] { return run.load(); }, Milliseconds(25)) + .timedOut()); +} + +// Test that the target's destruction stops the watch. +TEST_F(TestHandleWatcher, TargetDestroyed) { + WindowsEventObject event; + HandleWatcher hw; + bool run = false; + + auto queue = SpawnNewThread("target thread"); + hw.Watch(event.handle, queue.get(), + NS_NewRunnableFunction("never called", [&] { run = true; })); + + ASSERT_FALSE(hw.IsStopped()); + // synchronous shutdown before destruction + queue->Shutdown(); + + ASSERT_TRUE(hw.IsStopped()); + ASSERT_FALSE(run); +} + +// Test that calling `Watch` again stops the current watch. +TEST_F(TestHandleWatcher, Rewatch) { + WindowsEventObject event; + HandleWatcher hw; + + bool b1 = false; + bool b2 = false; + + { + auto queue = SpawnNewThread("target thread"); + + hw.Watch(event.handle, queue.get(), NS_NewRunnableFunction("b1", [&] { + b1 = true; + PingMainThread(); + })); + hw.Watch(event.handle, queue.get(), NS_NewRunnableFunction("b2", [&] { + b2 = true; + PingMainThread(); + })); + + event.Set(); + + ASSERT_TRUE(SpinEventLoopUntil([&] { return b1 || b2; }).ok()); + queue->Shutdown(); + } + ASSERT_FALSE(b1); + ASSERT_TRUE(b2); +} + +// Test that watching a HANDLE which is _already_ signaled still fires the +// associated task. +TEST_F(TestHandleWatcher, Presignalled) { + WindowsEventObject event; + HandleWatcher hw; + + bool run = false; + event.Set(); + hw.Watch(event.handle, NS_GetCurrentThread(), + NS_NewRunnableFunction("TestHandleWatcher::Presignalled", + [&] { run = true; })); + + ASSERT_TRUE(SpinEventLoopUntil([&] { return run; }).ok()); +} + +/////////////////////////////////////////////////////////////////////// +// Systematic tests: normal activation +// +// Test that a handle becoming signalled on target A correctly enqueues a task +// on target B, regardless of whether A == B. +// +struct ActivationTestSetup { + enum TargetType { Main, Side, Background }; + + WindowsEventObject event; + HandleWatcher watcher; + + std::atomic<bool> run = false; + nsCOMPtr<nsIThread> sideThread; + nsCOMPtr<nsISerialEventTarget> backgroundQueue; + + private: + nsIEventTarget* GetQueue(TargetType targetTyoe) { + MOZ_ASSERT(NS_IsMainThread()); + switch (targetTyoe) { + case TargetType::Main: + return NS_GetCurrentThread(); + + case TargetType::Side: { + if (!sideThread) { + sideThread = SpawnNewThread("side thread"); + } + return sideThread; + } + + case TargetType::Background: { + if (!backgroundQueue) { + backgroundQueue = SpawnNewBackgroundQueue(); + } + return backgroundQueue.get(); + } + } + } + + void OnSignaled() { + run = true; + // If we're not running on the main thread, it may be blocked waiting for + // events. + PingMainThread(); + } + + public: + void Setup(TargetType from, TargetType to) { + watcher.Watch( + event.handle, GetQueue(to), + NS_NewRunnableFunction("Reaction", [this] { this->OnSignaled(); })); + + MOZ_ALWAYS_SUCCEEDS(GetQueue(from)->Dispatch( + NS_NewRunnableFunction("Action", [this] { event.Set(); }))); + } + + bool Execute() { + MOZ_ASSERT(NS_IsMainThread()); + bool const spin = SpinEventLoopUntil([this] { + MOZ_ASSERT(NS_IsMainThread()); + return run.load(); + }).ok(); + return spin && watcher.IsStopped(); + } + + ~ActivationTestSetup() { watcher.Stop(); } +}; + +#define MOZ_HANDLEWATCHER_GTEST_FROM_TO(FROM, TO) \ + TEST_F(TestHandleWatcher, FROM##To##TO) { \ + ActivationTestSetup s; \ + s.Setup(ActivationTestSetup::TargetType::FROM, \ + ActivationTestSetup::TargetType::TO); \ + ASSERT_TRUE(s.Execute()); \ + } + +// Note that `Main -> Main` is subtly different from `Simple`, above: `Simple` +// sets the event before spinning, while `Main -> Main` merely enqueues a Task +// that will set the event during the spin. +MOZ_HANDLEWATCHER_GTEST_FROM_TO(Main, Main); +MOZ_HANDLEWATCHER_GTEST_FROM_TO(Main, Side); +MOZ_HANDLEWATCHER_GTEST_FROM_TO(Main, Background); +MOZ_HANDLEWATCHER_GTEST_FROM_TO(Side, Main); +MOZ_HANDLEWATCHER_GTEST_FROM_TO(Side, Side); +MOZ_HANDLEWATCHER_GTEST_FROM_TO(Side, Background); +MOZ_HANDLEWATCHER_GTEST_FROM_TO(Background, Main); +MOZ_HANDLEWATCHER_GTEST_FROM_TO(Background, Side); +MOZ_HANDLEWATCHER_GTEST_FROM_TO(Background, Background); + +/////////////////////////////////////////////////////////////////////// +// Ad-hoc tests: reentrancy +// +// Test that HandleWatcher neither deadlocks nor loses data if its release of a +// referenced object causes the invocation of another method on HandleWatcher. + +// Reentrancy case 1/2: the event target. +namespace { +class MockEventTarget final : public nsIEventTarget { + NS_DECL_THREADSAFE_ISUPPORTS + + private: + // Map from registered shutdown tasks to whether or not they have been (or are + // being) executed. (This should probably guarantee some deterministic order, + // and also be mutex-protected; but that doesn't matter here.) + nsTHashMap<RefPtr<nsITargetShutdownTask>, bool> mShutdownTasks; + // Out-of band task to be run last at destruction time, regardless of anything + // else. + std::function<void(void)> mDeathAction; + + ~MockEventTarget() { + for (auto& task : mShutdownTasks) { + task.SetData(true); + task.GetKey()->TargetShutdown(); + } + if (mDeathAction) { + mDeathAction(); + } + } + + public: + // shutdown task handling + NS_IMETHOD RegisterShutdownTask(nsITargetShutdownTask* task) override { + mShutdownTasks.WithEntryHandle(task, [&](auto entry) { + if (entry.HasEntry()) { + MOZ_CRASH("attempted to double-register shutdown task"); + } + entry.Insert(false); + }); + return NS_OK; + } + NS_IMETHOD UnregisterShutdownTask(nsITargetShutdownTask* task) override { + mozilla::Maybe<bool> res = mShutdownTasks.Extract(task); + if (!res.isSome()) { + MOZ_CRASH("attempted to unregister non-registered task"); + } + if (res.value()) { + MOZ_CRASH("attempted to unregister already-executed shutdown task"); + } + return NS_OK; + } + void RegisterDeathAction(std::function<void(void)>&& f) { + mDeathAction = std::move(f); + } + + // other nsIEventTarget methods (that we don't actually use) + NS_IMETHOD_(bool) IsOnCurrentThreadInfallible(void) { return false; } + NS_IMETHOD IsOnCurrentThread(bool* _retval) { + *_retval = false; + return NS_OK; + } + NS_IMETHOD Dispatch(already_AddRefed<nsIRunnable>, uint32_t) { + return NS_ERROR_NOT_IMPLEMENTED; + } + NS_IMETHOD DispatchFromScript(nsIRunnable*, uint32_t) { + return NS_ERROR_NOT_IMPLEMENTED; + } + NS_IMETHOD DelayedDispatch(already_AddRefed<nsIRunnable>, uint32_t) { + return NS_ERROR_NOT_IMPLEMENTED; + } +}; +NS_IMPL_ISUPPORTS(MockEventTarget, nsIEventTarget) +} // anonymous namespace + +// Test that a HandleWatcher neither deadlocks nor loses data if it's invoked +// when it releases its target. +TEST_F(TestHandleWatcher, TargetDestructionRecurrency) { + WindowsEventObject e1, e2; + bool b1 = false, b2 = false; + HandleWatcher hw; + + { + RefPtr<MockEventTarget> p = mozilla::MakeRefPtr<MockEventTarget>(); + + hw.Watch(e1.handle, p.get(), NS_NewRunnableFunction("first callback", [&] { + b1 = true; + PingMainThread(); + })); + + p->RegisterDeathAction([&] { + hw.Watch(e2.handle, mozilla::GetMainThreadSerialEventTarget(), + NS_NewRunnableFunction("second callback", [&] { b2 = true; })); + }); + } + + ASSERT_FALSE(hw.IsStopped()); + hw.Stop(); + ASSERT_FALSE(hw.IsStopped()); // [sic] + + e1.Set(); // should do nothing + e2.Set(); + ASSERT_TRUE(SpinEventLoopUntil([&] { return b1 || b2; }).ok()); + ASSERT_FALSE(b1); + ASSERT_TRUE(b2); +} + +// Reentrancy case 2/2: the runnable. +namespace { +class MockRunnable final : public nsIRunnable { + NS_DECL_THREADSAFE_ISUPPORTS + NS_IMETHOD Run() override { + MOZ_CRASH("MockRunnable was invoked"); + return NS_ERROR_NOT_IMPLEMENTED; + } + + std::function<void(void)> mDeathAction; + + public: + void RegisterDeathAction(std::function<void(void)>&& f) { + mDeathAction = std::move(f); + } + + private: + ~MockRunnable() { + if (mDeathAction) { + mDeathAction(); + } + } +}; +NS_IMPL_ISUPPORTS(MockRunnable, nsIRunnable) +} // anonymous namespace + +// Test that a HandleWatcher neither deadlocks nor loses data if it's invoked +// when it releases its task. +TEST_F(TestHandleWatcher, TaskDestructionRecurrency) { + WindowsEventObject e1, e2; + bool run = false; + HandleWatcher hw; + + auto thread = SpawnNewBackgroundQueue(); + + { + RefPtr<MockRunnable> runnable = mozilla::MakeRefPtr<MockRunnable>(); + + runnable->RegisterDeathAction([&] { + hw.Watch(e2.handle, thread, NS_NewRunnableFunction("callback", [&] { + run = true; + PingMainThread(); + })); + }); + + hw.Watch(e1.handle, thread.get(), runnable.forget()); + } + + ASSERT_FALSE(hw.IsStopped()); + hw.Stop(); + ASSERT_FALSE(hw.IsStopped()); // [sic] + + e1.Set(); + // give MockRunnable a chance to run (and therefore crash) if it somehow + // hasn't been discmnnected + ASSERT_TRUE( + SpinEventLoopUntil([&] { return false; }, Milliseconds(10)).timedOut()); + + e2.Set(); + ASSERT_TRUE(SpinEventLoopUntil([&] { return run; }).ok()); + ASSERT_TRUE(run); +} |