582 lines
18 KiB
C++
582 lines
18 KiB
C++
/* -*- 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_RELEASE_ASSERT(
|
|
sIsLive,
|
|
"attempted to use `class TestHandleWatcher` outside test group");
|
|
}
|
|
|
|
static RefPtr<mozilla::SharedThreadPool> sPool;
|
|
};
|
|
|
|
/* static */
|
|
bool TestHandleWatcher::sIsLive = false;
|
|
/* static */
|
|
MOZ_RUNINIT 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_RELEASE_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_RELEASE_ASSERT(NS_IsMainThread());
|
|
bool const spin = SpinEventLoopUntil([this] {
|
|
MOZ_RELEASE_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);
|
|
}
|