google / google/googletest

[FR]: Coroutines as first class citizens

Open
#4,649 3 comments 9 reactions 0 assignees View on GitHub
enhancement
Dominant language
C++
Stars
39.5k
Forks
10.9k
Avg merge
6d 13h
Merged PRs (30d)
1

Description

### 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:
```cpp
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):
```cpp
#define CO_TEST_F(test_fixture, test_name) \
CO_GTEST_TEST_(test_fixture, test_name, test_fixture, \
::testing::internal::GetTypeId())

#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:
```cpp
struct MyCoro
{
struct promise_type
{
auto get_return_object() -> MyCoro
{
return {std::coroutine_handle::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 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.

Contributor guide

Open the contributing guide

Research direction

Start by reading the existing TEST/TEST_F expansion and ASSERT_* macro implementations, then compare them with the proposed C++20 coroutine test shape. The proposal leaves the coroutine type integration and coroutine-safe assertion behavior unresolved. Done means agreeing on that design and defining a tested, usable coroutine equivalent for the requested test macros.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
testing-qa
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.