async: context: handle shutdown better

When the context has been requested to stop we don't actually want
to stop it until all work has completed.  Work that is pending
could require the dbus/event to function properly so we need to keep
that running until that time.  If we shutdown those too early, we
can end up with a spawned task which never completes because it is
relying on an event which is never processed.

In order to keep all of these straight, split the scope/stop_tokens
into two: pending and internal.  Pending tasks are for those 'spawned'
due to the user requests and internal tasks are those maintained by the
context itself to aid in processing.  Defer shutting down the
dbus/event handlers until all 'pending' work is completed.

Add a test-case which use to hang and now completes.

Signed-off-by: Patrick Williams <patrick@stwcx.xyz>
Change-Id: I399d065ae15b24ad3971c526c27b73757af14b34
diff --git a/test/async/context.cpp b/test/async/context.cpp
index 83cfde4..4092de5 100644
--- a/test/async/context.cpp
+++ b/test/async/context.cpp
@@ -2,19 +2,51 @@
 
 #include <gtest/gtest.h>
 
-TEST(Context, RunSimple)
+struct Context : public testing::Test
 {
-    sdbusplus::async::context ctx;
-    ctx.run(std::execution::just() |
-            std::execution::then([&ctx]() { ctx.request_stop(); }));
+    ~Context() noexcept = default;
+
+    void TearDown() override
+    {
+        // Destructing the context can throw, so we have to do it in
+        // the TearDown in order to make our destructor noexcept.
+        ctx.reset();
+    }
+
+    std::optional<sdbusplus::async::context> ctx{std::in_place};
+};
+
+TEST_F(Context, RunSimple)
+{
+    ctx->run(std::execution::just() |
+             std::execution::then([this]() { ctx->request_stop(); }));
 }
 
-TEST(Context, SpawnedTask)
+TEST_F(Context, SpawnedTask)
 {
-    sdbusplus::async::context ctx;
+    ctx->spawn(std::execution::just());
 
-    ctx.spawn(std::execution::just());
+    ctx->run(std::execution::just() |
+             std::execution::then([this]() { ctx->request_stop(); }));
+}
 
-    ctx.run(std::execution::just() |
-            std::execution::then([&ctx]() { ctx.request_stop(); }));
+TEST_F(Context, SpawnDelayedTask)
+{
+    using namespace std::literals;
+    static constexpr auto timeout = 500ms;
+
+    auto start = std::chrono::steady_clock::now();
+
+    bool ran = false;
+    ctx->spawn(sdbusplus::async::sleep_for(*ctx, timeout) |
+               std::execution::then([&ran]() { ran = true; }));
+
+    ctx->run(std::execution::just() |
+             std::execution::then([this]() { ctx->request_stop(); }));
+
+    auto stop = std::chrono::steady_clock::now();
+
+    EXPECT_TRUE(ran);
+    EXPECT_GT(stop - start, timeout);
+    EXPECT_LT(stop - start, timeout * 2);
 }