Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FR]: Coroutines as first class citizens #4649

Open
kanje opened this issue Oct 26, 2024 · 0 comments
Open

[FR]: Coroutines as first class citizens #4649

kanje opened this issue Oct 26, 2024 · 0 comments

Comments

@kanje
Copy link

kanje commented Oct 26, 2024

Does the feature exist in the most recent commit?

No.

Why do we need this feature?

Coroutines are available since C++20 which was published almost four years ago. One of the coroutine use-cases is to do asynchronous I/O. Number of codebases doing this gets larger and larger. And yet, there is no native support to test such code with GTest.

Consider this example/wish:

TEST(ImageDownloaderTest, example)
{
    setUpMocks();
    auto url = co_await resolveImageUrl("my-image");
    ASSERT_EQ(url, "https://....");
    auto img = co_await downloadImage(url);
    ASSERT_EQ(img.size(), 2048);
}

I say "native support" because in principle such code is already testable, but it requires to repeat the same boiler plate for each test case. It is also not possible to use ASSERT_* macros because they internally use return. This cannot be fixed on client side without defining custom macros which would rely on internal details of GTest.

Describe the proposal.

Define CO_TEST and CO_TEST_F macros to be coroutine versions of resp. TEST and TEST_F. They can be possibly implemented as (some details are omitted):

#define CO_TEST_F(test_fixture, test_name)                                                         \
    CO_GTEST_TEST_(test_fixture, test_name, test_fixture,                                          \
                   ::testing::internal::GetTypeId<test_fixture>())

#define CO_GTEST_TEST_(test_suite_name, test_name, parent_class, parent_id)                        \
    class GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                                       \
        : public parent_class                                                                      \
    {                                                                                              \
    private:                                                                                       \
        void TestBody() override;                                                                  \
        MyCoro CoTestBody();                                                                       \
    };                                                                                             \
    void GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)::TestBody()                            \
    {                                                                                              \
        auto coro = CoTestBody();                                                                  \
        coro.spawn();                                                                              \
        EXPECT_TRUE(coro.handle.done()) << "Coroutine is not finished";                            \
        coro.handle.destroy();                                                                     \
    }                                                                                              \
    MyCoro GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)::CoTestBody()

Boilerplate code is hidden inside TestBody() and the test itself is defined inside CoTestBody(). The coroutine type (here MyCoro) needs to be customizable because of the spawn() method. It is intended to start the machinery needed to execute coroutines (e.g. run an event loop) and this action would be different for different coroutine libraries/frameworks used in a project. I would leave the exact way, how to achieve this, our of scope for now.

One possible coroutine type implementation:

struct MyCoro
{
    struct promise_type
    {
        auto get_return_object() -> MyCoro
        {
            return {std::coroutine_handle<promise_type>::from_promise(*this)};
        }

        auto initial_suspend() -> std::suspend_never { return {}; }
        auto final_suspend() noexcept -> std::suspend_always { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };

    void spawn() const
    {
        runEventLoop();
    }

    std::coroutine_handle<promise_type> handle;
};

Finally, define CO_ASSERT_* macros as coroutine friendly versions of ASSERT_*. They would do co_return instead of return. Or is there a way to distinguish if a macro is invoked inside a coroutine vs. a normal function?

Is the feature specific to an operating system, compiler, or build system version?

C++20 or later.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant