Run a glib main loop in a ShmEventLoop
This makes it easy to handle events from both in the same context
without dealing with threading.
Change-Id: Id7dc140e0bc21facf965a43142b1b7d35ffcd82b
Signed-off-by: Brian Silverman <bsilver16384@gmail.com>
diff --git a/aos/events/BUILD b/aos/events/BUILD
index e27e775..0f19beb 100644
--- a/aos/events/BUILD
+++ b/aos/events/BUILD
@@ -424,3 +424,38 @@
":event_loop",
],
)
+
+cc_library(
+ name = "glib_main_loop",
+ srcs = [
+ "glib_main_loop.cc",
+ ],
+ hdrs = [
+ "glib_main_loop.h",
+ ],
+ visibility = ["//visibility:public"],
+ deps = [
+ "//aos/events:shm_event_loop",
+ "//third_party:gstreamer",
+ "@com_github_google_glog//:glog",
+ ],
+)
+
+cc_test(
+ name = "glib_main_loop_test",
+ srcs = [
+ "glib_main_loop_test.cc",
+ ],
+ data = [
+ ":config",
+ ],
+ deps = [
+ ":glib_main_loop",
+ "//aos:configuration",
+ "//aos/events:shm_event_loop",
+ "//aos/testing:googletest",
+ "//aos/testing:path",
+ "//third_party:gstreamer",
+ "@com_github_google_glog//:glog",
+ ],
+)
diff --git a/aos/events/glib_main_loop.cc b/aos/events/glib_main_loop.cc
new file mode 100644
index 0000000..6bac983
--- /dev/null
+++ b/aos/events/glib_main_loop.cc
@@ -0,0 +1,186 @@
+#include "aos/events/glib_main_loop.h"
+
+#include "glog/logging.h"
+
+namespace aos {
+namespace {
+
+gint EpollToGio(uint32_t epoll) {
+ gint result = 0;
+ if (epoll & EPOLLIN) {
+ result |= G_IO_IN;
+ epoll &= ~EPOLLIN;
+ }
+ if (epoll & EPOLLOUT) {
+ result |= G_IO_OUT;
+ epoll &= ~EPOLLOUT;
+ }
+ if (epoll & (EPOLLRDHUP | EPOLLHUP)) {
+ result |= G_IO_HUP;
+ epoll &= ~(EPOLLRDHUP | EPOLLHUP);
+ }
+ if (epoll & EPOLLPRI) {
+ result |= G_IO_PRI;
+ epoll &= ~EPOLLPRI;
+ }
+ if (epoll & EPOLLERR) {
+ result |= G_IO_ERR;
+ epoll &= ~EPOLLERR;
+ }
+ CHECK_EQ(epoll, 0u) << ": Unhandled epoll bits";
+ return result;
+}
+
+uint32_t GioToEpoll(gint gio) {
+ uint32_t result = 0;
+ if (gio & G_IO_IN) {
+ result |= EPOLLIN;
+ gio &= ~G_IO_IN;
+ }
+ if (gio & G_IO_OUT) {
+ result |= EPOLLOUT;
+ gio &= ~G_IO_OUT;
+ }
+ if (gio & G_IO_HUP) {
+ result |= EPOLLHUP;
+ gio &= ~G_IO_HUP;
+ }
+ if (gio & G_IO_PRI) {
+ result |= EPOLLPRI;
+ gio &= ~G_IO_PRI;
+ }
+ if (gio & G_IO_ERR) {
+ result |= EPOLLERR;
+ gio &= ~G_IO_ERR;
+ }
+ CHECK_EQ(gio, 0) << ": Unhandled gio bits";
+ return result;
+}
+
+} // namespace
+
+GlibMainLoop::GlibMainLoop(ShmEventLoop *event_loop)
+ : event_loop_(event_loop),
+ timeout_timer_(event_loop->AddTimer([]() {
+ // Don't need to do anything, just need to get the event loop to break
+ // out of the kernel and call BeforeWait again.
+ })),
+ g_main_context_(g_main_context_ref(g_main_context_default())),
+ g_main_loop_(g_main_loop_new(g_main_context_, true)) {
+ event_loop_->OnRun([this]() {
+ CHECK(!acquired_context_);
+ CHECK(g_main_context_acquire(g_main_context_))
+ << ": The EventLoop thread must own the context";
+ acquired_context_ = true;
+ });
+ event_loop_->epoll()->BeforeWait([this]() { BeforeWait(); });
+}
+
+GlibMainLoop::~GlibMainLoop() {
+ CHECK_EQ(children_, 0) << ": Failed to destroy all children";
+ RemoveAllFds();
+ if (acquired_context_) {
+ g_main_context_release(g_main_context_);
+ }
+ g_main_loop_unref(g_main_loop_);
+ g_main_context_unref(g_main_context_);
+}
+
+void GlibMainLoop::RemoveAllFds() {
+ while (true) {
+ const auto to_remove = added_fds_.begin();
+ if (to_remove == added_fds_.end()) {
+ break;
+ }
+ event_loop_->epoll()->DeleteFd(*to_remove);
+ added_fds_.erase(to_remove);
+ }
+}
+
+void GlibMainLoop::BeforeWait() {
+ if (!g_main_loop_is_running(g_main_loop_)) {
+ // glib will never quiesce its FDs, so the best we can do is just skip it
+ // once it's done and shut down our event loop. We have to remove all of its
+ // FDs first so other event sources can quiesce.
+ VLOG(1) << "g_main_loop_is_running = false";
+ RemoveAllFds();
+ event_loop_->Exit();
+ return;
+ }
+ if (!event_loop_->epoll()->should_run()) {
+ // Give glib one more round of dispatching.
+ VLOG(1) << "EPoll::should_run = false";
+ g_main_loop_quit(g_main_loop_);
+ }
+
+ if (!gpoll_fds_.empty()) {
+ // Tell glib about any events we received on the FDs it asked about.
+ if (g_main_context_check(g_main_context_, last_query_max_priority_,
+ gpoll_fds_.data(), gpoll_fds_.size())) {
+ VLOG(1) << "g_main_context_dispatch";
+ // We have some glib events now, dispatch them now.
+ g_main_context_dispatch(g_main_context_);
+ }
+ }
+
+ // Call prepare to check for any other events that are ready to be dispatched.
+ // g_main_context_iterate ignores the return value, so we're going to do that
+ // too.
+ g_main_context_prepare(g_main_context_, &last_query_max_priority_);
+
+ gint timeout_ms;
+ while (true) {
+ const gint number_new_fds =
+ g_main_context_query(g_main_context_, last_query_max_priority_,
+ &timeout_ms, gpoll_fds_.data(), gpoll_fds_.size());
+ if (static_cast<size_t>(number_new_fds) <= gpoll_fds_.size()) {
+ // They all fit, resize to drop any stale entries and then we're done.
+ gpoll_fds_.resize(number_new_fds);
+ VLOG(1) << "glib gave " << number_new_fds;
+ break;
+ }
+ // Need more space, we know how much so try again.
+ gpoll_fds_.resize(number_new_fds);
+ }
+
+ for (GPollFD gpoll_fd : gpoll_fds_) {
+ // API docs are a bit unclear, but this shouldn't ever happen I think?
+ CHECK_EQ(gpoll_fd.revents, 0) << ": what does this mean?";
+
+ if (added_fds_.count(gpoll_fd.fd) == 0) {
+ VLOG(1) << "Add to ShmEventLoop: " << gpoll_fd.fd;
+ event_loop_->epoll()->OnEvents(
+ gpoll_fd.fd, [this, fd = gpoll_fd.fd](uint32_t events) {
+ VLOG(1) << "glib " << fd << " triggered: " << std::hex << events;
+ const auto iterator = std::find_if(
+ gpoll_fds_.begin(), gpoll_fds_.end(),
+ [fd](const GPollFD &candidate) { return candidate.fd == fd; });
+ CHECK(iterator != gpoll_fds_.end())
+ << ": Lost GPollFD for " << fd
+ << " but still registered with epoll";
+ iterator->revents |= EpollToGio(events);
+ });
+ added_fds_.insert(gpoll_fd.fd);
+ }
+ event_loop_->epoll()->SetEvents(gpoll_fd.fd, GioToEpoll(gpoll_fd.events));
+ }
+ for (int fd : added_fds_) {
+ const auto iterator = std::find_if(
+ gpoll_fds_.begin(), gpoll_fds_.end(),
+ [fd](const GPollFD &candidate) { return candidate.fd == fd; });
+ if (iterator == gpoll_fds_.end()) {
+ VLOG(1) << "Remove from ShmEventLoop: " << fd;
+ added_fds_.erase(fd);
+ }
+ }
+ CHECK_EQ(added_fds_.size(), gpoll_fds_.size());
+ VLOG(1) << "Timeout: " << timeout_ms;
+ if (timeout_ms == -1) {
+ timeout_timer_->Disable();
+ } else {
+ timeout_timer_->Setup(event_loop_->monotonic_now() +
+ std::chrono::milliseconds(timeout_ms));
+ }
+}
+
+} // namespace aos
diff --git a/aos/events/glib_main_loop.h b/aos/events/glib_main_loop.h
new file mode 100644
index 0000000..080bc57
--- /dev/null
+++ b/aos/events/glib_main_loop.h
@@ -0,0 +1,295 @@
+#ifndef AOS_EVENTS_GLIB_MAIN_LOOP_H_
+#define AOS_EVENTS_GLIB_MAIN_LOOP_H_
+
+#include <glib-object.h>
+#include <glib.h>
+
+#include <unordered_set>
+
+#include "aos/events/shm_event_loop.h"
+
+namespace aos {
+
+class GlibMainLoop;
+
+// Adapts a std::function to a g_source-style callback.
+//
+// T is the function pointer type.
+//
+// This doesn't interact with a GlibMainLoop, so it's safe to use from any
+// thread, but it also won't catch lifetime bugs cleanly.
+template <typename T>
+class GlibSourceCallback {
+ private:
+ template <typename TResult, typename... TArgs>
+ struct helper {
+ static TResult Invoke(TArgs... args, gpointer user_data) {
+ GlibSourceCallback *const pointer =
+ reinterpret_cast<GlibSourceCallback *>(user_data);
+ CHECK(g_main_context_is_owner(pointer->g_main_context_))
+ << ": Callback being called from the wrong thread";
+ return pointer->function_(args...);
+ }
+ using Function = std::function<TResult(TArgs...)>;
+ };
+ // A helper to deduce template arguments (type template arguments can't be
+ // deduced, so we need a function).
+ template <typename TResult>
+ static helper<TResult> MakeHelper(TResult (*)(gpointer)) {
+ return helper<TResult>();
+ }
+
+ using HelperType =
+ decltype(GlibSourceCallback::MakeHelper(std::declval<T>()));
+
+ protected:
+ using Function = typename HelperType::Function;
+
+ public:
+ // May be called from any thread.
+ GlibSourceCallback(Function function, GSource *source,
+ GMainContext *g_main_context);
+ // May only be called from the main thread.
+ ~GlibSourceCallback();
+
+ // Instances may not be moved because a pointer to the instance gets passed
+ // around.
+ GlibSourceCallback(const GlibSourceCallback &) = delete;
+ GlibSourceCallback &operator=(const GlibSourceCallback &) = delete;
+
+ private:
+ GSourceFunc g_source_func() const {
+ return reinterpret_cast<GSourceFunc>(&HelperType::Invoke);
+ }
+ gpointer user_data() const {
+ return const_cast<gpointer>(reinterpret_cast<const void *>(this));
+ }
+
+ const Function function_;
+ GSource *const source_;
+ GMainContext *const g_main_context_;
+};
+
+template <typename T>
+class GlibSourceCallbackRefcount : public GlibSourceCallback<T> {
+ public:
+ GlibSourceCallbackRefcount(typename GlibSourceCallback<T>::Function function,
+ GSource *source, GlibMainLoop *glib_main_loop);
+ ~GlibSourceCallbackRefcount();
+
+ private:
+ GlibMainLoop *const glib_main_loop_;
+};
+
+// Adapts a std::function to a g_signal-style callback. This includes calling
+// the std::function the main thread, vs the g_signal callback is invoked on an
+// arbitrary thread.
+template <typename... Args>
+class GlibSignalCallback {
+ public:
+ GlibSignalCallback(std::function<void(Args...)> function,
+ GlibMainLoop *glib_main_loop, gpointer instance,
+ const char *detailed_signal);
+ ~GlibSignalCallback();
+
+ // Instances may not be moved because a pointer to the instance gets passed
+ // around.
+ GlibSignalCallback(const GlibSignalCallback &) = delete;
+ GlibSignalCallback &operator=(const GlibSignalCallback &) = delete;
+
+ private:
+ static void InvokeSignal(Args... args, gpointer user_data);
+ gpointer user_data() const {
+ return const_cast<gpointer>(reinterpret_cast<const void *>(this));
+ }
+
+ const std::function<void(Args...)> function_;
+ GlibMainLoop *const glib_main_loop_;
+ const gpointer instance_;
+ const gulong signal_handler_id_;
+
+ // Protects changes to invocations_ and idle_callback_.
+ aos::stl_mutex lock_;
+ std::vector<std::tuple<Args...>> invocations_;
+ std::optional<GlibSourceCallback<GSourceFunc>> idle_callback_;
+};
+
+// Manages a GMainLoop attached to a ShmEventLoop.
+//
+// Also provides C++ RAII wrappers around the related glib objects.
+class GlibMainLoop {
+ public:
+ GlibMainLoop(ShmEventLoop *event_loop);
+ ~GlibMainLoop();
+ GlibMainLoop(const GlibMainLoop &) = delete;
+ GlibMainLoop &operator=(const GlibMainLoop &) = delete;
+
+ GMainContext *g_main_context() { return g_main_context_; }
+ GMainLoop *g_main_loop() { return g_main_loop_; }
+
+ auto AddIdle(std::function<gboolean()> callback) {
+ return GlibSourceCallbackRefcount<GSourceFunc>(std::move(callback),
+ g_idle_source_new(), this);
+ }
+
+ auto AddTimeout(std::function<gboolean()> callback, guint interval) {
+ return GlibSourceCallbackRefcount<GSourceFunc>(
+ std::move(callback), g_timeout_source_new(interval), this);
+ }
+
+ // Connects a glib signal to a callback. Note that this is NOT a Unix signal.
+ //
+ // Note that the underlying signal handler is called in one of gstreamer's
+ // thread, but callback will be called in the main thread. This means that any
+ // objects being passed in with the expectation of the handler incrementing
+ // their refcount need special handling. This also means any glib signal
+ // handlers which need to return a value cannot use this abstraction.
+ //
+ // It's recommended to pass an actual std::function (NOT something with a
+ // user-defined conversion to a std::function, such as a lambda) as the first
+ // argument, which allows the template arguments to be deduced.
+ //
+ // Passing a lambda with explicit template arguments doesn't work
+ // unfortunately... I think it's because the variadic template argument could
+ // be extended beyond anything you explicitly pass in, so it's always doing
+ // deduction, and deduction doesn't consider the user-defined conversion
+ // between the lambda's type and the relevant std::function type. C++ sucks,
+ // sorry.
+ template <typename... Args>
+ auto ConnectSignal(std::function<void(Args...)> callback, gpointer instance,
+ const char *detailed_signal) {
+ return GlibSignalCallback<Args...>(std::move(callback), this, instance,
+ detailed_signal);
+ }
+
+ void AddChild() { ++children_; }
+
+ void RemoveChild() {
+ CHECK_GT(children_, 0);
+ --children_;
+ }
+
+ private:
+ void RemoveAllFds();
+ void BeforeWait();
+
+ // fds which we have added to the epoll object.
+ std::unordered_set<int> added_fds_;
+
+ ShmEventLoop *const event_loop_;
+ TimerHandler *const timeout_timer_;
+ GMainContext *const g_main_context_;
+ GMainLoop *const g_main_loop_;
+
+ // The list of FDs and priority received from glib on the latest
+ // g_main_context_query call, so we can pass them to the g_main_context_check
+ // next time around.
+ std::vector<GPollFD> gpoll_fds_;
+ gint last_query_max_priority_;
+
+ // Tracks whether we did the call to acquire g_main_context_.
+ bool acquired_context_ = false;
+
+ // Tracking all the child glib objects we create. None of them should outlive
+ // us, and asserting that helps catch bugs in application code that leads to
+ // use-after-frees.
+ int children_ = 0;
+};
+
+template <typename T>
+inline GlibSourceCallback<T>::GlibSourceCallback(Function function,
+ GSource *source,
+ GMainContext *g_main_context)
+ : function_(function), source_(source), g_main_context_(g_main_context) {
+ g_source_set_callback(source_, g_source_func(), user_data(), nullptr);
+ CHECK_GT(g_source_attach(source_, g_main_context_), 0u);
+ VLOG(1) << "Attached source " << source_ << " to " << g_main_context_;
+}
+
+template <typename T>
+inline GlibSourceCallback<T>::~GlibSourceCallback() {
+ CHECK(g_main_context_is_owner(g_main_context_))
+ << ": May only be destroyed from the main thread";
+
+ g_source_destroy(source_);
+ VLOG(1) << "Destroyed source " << source_;
+ // Now, the callback won't be called any more (because this source is no
+ // longer attached to a context), even if refcounts remain that hold the
+ // source itself alive. That's not safe in a multithreaded context, but we
+ // only allow this operation in the main thread, which means it synchronizes
+ // with any other code in the main thread that might call the callback.
+
+ g_source_unref(source_);
+}
+
+template <typename T>
+GlibSourceCallbackRefcount<T>::GlibSourceCallbackRefcount(
+ typename GlibSourceCallback<T>::Function function, GSource *source,
+ GlibMainLoop *glib_main_loop)
+ : GlibSourceCallback<T>(std::move(function), source,
+ glib_main_loop->g_main_context()),
+ glib_main_loop_(glib_main_loop) {
+ glib_main_loop_->AddChild();
+}
+
+template <typename T>
+GlibSourceCallbackRefcount<T>::~GlibSourceCallbackRefcount() {
+ glib_main_loop_->RemoveChild();
+}
+
+template <typename... Args>
+GlibSignalCallback<Args...>::GlibSignalCallback(
+ std::function<void(Args...)> function, GlibMainLoop *glib_main_loop,
+ gpointer instance, const char *detailed_signal)
+ : function_(std::move(function)),
+ glib_main_loop_(glib_main_loop),
+ instance_(instance),
+ signal_handler_id_(g_signal_connect(
+ instance, detailed_signal,
+ G_CALLBACK(&GlibSignalCallback::InvokeSignal), user_data())) {
+ CHECK_GT(signal_handler_id_, 0);
+ VLOG(1) << this << " connected glib signal with " << user_data() << " as "
+ << signal_handler_id_ << " on " << instance << ": "
+ << detailed_signal;
+ glib_main_loop_->AddChild();
+}
+
+template <typename... Args>
+GlibSignalCallback<Args...>::~GlibSignalCallback() {
+ g_signal_handler_disconnect(instance_, signal_handler_id_);
+ VLOG(1) << this << " disconnected glib signal on " << instance_ << ": "
+ << signal_handler_id_;
+ glib_main_loop_->RemoveChild();
+}
+
+template <typename... Args>
+void GlibSignalCallback<Args...>::InvokeSignal(Args... args,
+ gpointer user_data) {
+ CHECK(user_data != nullptr) << ": invalid glib signal callback";
+ GlibSignalCallback *const pointer =
+ reinterpret_cast<GlibSignalCallback *>(user_data);
+ VLOG(1) << "Adding invocation of signal " << pointer;
+ std::unique_lock<aos::stl_mutex> locker(pointer->lock_);
+ CHECK_EQ(!!pointer->idle_callback_, !pointer->invocations_.empty());
+ if (!pointer->idle_callback_) {
+ // If we don't already have a callback set, then schedule a new one.
+ pointer->idle_callback_.emplace(
+ [pointer]() {
+ std::unique_lock<aos::stl_mutex> locker(pointer->lock_);
+ for (const auto &args : pointer->invocations_) {
+ VLOG(1) << "Calling signal handler for " << pointer;
+ std::apply(pointer->function_, args);
+ }
+ pointer->invocations_.clear();
+ pointer->idle_callback_.reset();
+ return false;
+ },
+ g_idle_source_new(), pointer->glib_main_loop_->g_main_context());
+ }
+ pointer->invocations_.emplace_back(
+ std::make_tuple<Args...>(std::forward<Args>(args)...));
+}
+
+} // namespace aos
+
+#endif // AOS_EVENTS_GLIB_MAIN_LOOP_H_
diff --git a/aos/events/glib_main_loop_test.cc b/aos/events/glib_main_loop_test.cc
new file mode 100644
index 0000000..b857b0b
--- /dev/null
+++ b/aos/events/glib_main_loop_test.cc
@@ -0,0 +1,131 @@
+#include "aos/events/glib_main_loop.h"
+
+#include <thread>
+
+#include "aos/configuration.h"
+#include "aos/events/shm_event_loop.h"
+#include "aos/testing/path.h"
+#include "glib-2.0/glib.h"
+#include "glog/logging.h"
+#include "gtest/gtest.h"
+
+namespace aos {
+namespace testing {
+using aos::testing::ArtifactPath;
+
+const FlatbufferDetachedBuffer<Configuration> &Config() {
+ static const FlatbufferDetachedBuffer<Configuration> result =
+ configuration::ReadConfig(ArtifactPath("aos/events/config.json"));
+ return result;
+}
+
+// Tests just creating and destroying without running.
+TEST(GlibMainLoopTest, CreateDestroy) {
+ ShmEventLoop event_loop(Config());
+ GlibMainLoop glib_main_loop(&event_loop);
+}
+
+// Tests just creating, running, and then destroying, without adding any
+// events from the glib side.
+TEST(GlibMainLoopTest, CreateRunDestroy) {
+ ShmEventLoop event_loop(Config());
+ GlibMainLoop glib_main_loop(&event_loop);
+ bool ran = false;
+ event_loop
+ .AddTimer([&event_loop, &ran]() {
+ event_loop.Exit();
+ ran = true;
+ })
+ ->Setup(event_loop.monotonic_now() + std::chrono::milliseconds(100));
+ event_loop.Run();
+ EXPECT_TRUE(ran);
+}
+
+// Tests just a single idle source.
+TEST(GlibMainLoopTest, IdleSource) {
+ ShmEventLoop event_loop(Config());
+ GlibMainLoop glib_main_loop(&event_loop);
+ int runs = 0;
+ const auto callback =
+ glib_main_loop.AddIdle([&event_loop, &runs]() -> gboolean {
+ if (runs++ >= 100) {
+ event_loop.Exit();
+ }
+ return true;
+ });
+ event_loop.Run();
+ EXPECT_GT(runs, 100);
+ // It can run a few extra times, but not too many.
+ EXPECT_LT(runs, 110);
+}
+
+// Tests just a single timeout which calls exit on the ShmEventLoop side.
+TEST(GlibMainLoopTest, TimeoutExitShm) {
+ ShmEventLoop event_loop(Config());
+ GlibMainLoop glib_main_loop(&event_loop);
+ int runs = 0;
+ const auto callback = glib_main_loop.AddTimeout(
+ [&event_loop, &runs]() -> gboolean {
+ if (runs++ >= 3) {
+ event_loop.Exit();
+ }
+ return true;
+ },
+ 50);
+ const auto before = event_loop.monotonic_now();
+ event_loop.Run();
+ const auto after = event_loop.monotonic_now();
+ EXPECT_EQ(runs, 4);
+ // Verify it took at least this long, but don't bother putting an upper bound
+ // because it can take arbitrarily long due to scheduling delays.
+ EXPECT_GE(after - before, std::chrono::milliseconds(200));
+}
+
+// Tests just a single timeout which calls exit on the glib side.
+TEST(GlibMainLoopTest, TimeoutExitGlib) {
+ ShmEventLoop event_loop(Config());
+ GlibMainLoop glib_main_loop(&event_loop);
+ int runs = 0;
+ const auto callback = glib_main_loop.AddTimeout(
+ [&glib_main_loop, &runs]() -> gboolean {
+ if (runs++ >= 3) {
+ g_main_loop_quit(glib_main_loop.g_main_loop());
+ }
+ return true;
+ },
+ 50);
+ const auto before = event_loop.monotonic_now();
+ event_loop.Run();
+ const auto after = event_loop.monotonic_now();
+ EXPECT_EQ(runs, 4);
+ // Verify it took at least this long, but don't bother putting an upper bound
+ // because it can take arbitrarily long due to scheduling delays.
+ EXPECT_GE(after - before, std::chrono::milliseconds(200));
+}
+
+// Tests a single timeout which removes itself, and a ShmEventLoop timer to end
+// the test.
+TEST(GlibMainLoopTest, TimeoutRemoveSelf) {
+ ShmEventLoop event_loop(Config());
+ GlibMainLoop glib_main_loop(&event_loop);
+ int runs = 0;
+ const auto callback = glib_main_loop.AddTimeout(
+ [&runs]() -> gboolean {
+ ++runs;
+ return false;
+ },
+ 50);
+ bool ran = false;
+ event_loop
+ .AddTimer([&event_loop, &ran]() {
+ event_loop.Exit();
+ ran = true;
+ })
+ ->Setup(event_loop.monotonic_now() + std::chrono::milliseconds(100));
+ event_loop.Run();
+ EXPECT_TRUE(ran);
+ EXPECT_EQ(runs, 1);
+}
+
+} // namespace testing
+} // namespace aos