diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a0e326..26230b5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,7 @@ if(NOT CMAKE_CROSSCOMPILING AND BUILD_UNIT_TESTS) PRIVATE tests/main.test.cpp tests/basics.test.cpp + tests/basic_context.test.cpp tests/blocked_by.test.cpp tests/cancel.test.cpp tests/exclusive_access.test.cpp diff --git a/modules/async_context.cppm b/modules/async_context.cppm index e1d99e3..d9a61a0 100644 --- a/modules/async_context.cppm +++ b/modules/async_context.cppm @@ -868,23 +868,22 @@ private: * @note basic_context is designed for simple use cases and testing, not * production embedded systems where strict memory management is required. */ -export class basic_context : public context +class basic_context_impl : public context { public: - // TODO(63): Add stack memory template argument /** - * @brief Default constructor for basic_context + * @brief Default constructor for basic_context_impl * * Creates a new basic context with default initialization. */ - basic_context() = default; + basic_context_impl() = default; /** * @brief Virtual destructor for proper cleanup * * Ensures that the basic context is properly cleaned up when deleted. */ - ~basic_context() override = default; + ~basic_context_impl() override = default; /** * @brief Get the pending delay time for time-blocking operations @@ -969,6 +968,63 @@ private: sleep_duration m_pending_delay{ 0 }; }; +/** + * @brief A basic context implementation that supports synchronous waiting + * + * The basic_context class provides a concrete implementation of the context + * interface that supports synchronous waiting operations. It extends the base + * context with functionality to wait for coroutines to complete using a simple + * synchronous loop. + * + * This class provides stack memory via its `StackSizeInWords` template + * variable. + * + * @tparam StackSizeInWords - the number of words to allocate for the context's + * stack memory. Word size is 4 bytes for 32-bit systems and 8 bytes on 64-bit + * systems. + */ +export template +class basic_context +{ +public: + static_assert(StackSizeInWords > 0UL, + "Stack memory must be greater than 0 words."); + + basic_context() + { + m_context.initialize_stack_memory(m_stack); + } + + ~basic_context() + { + m_context.cancel(); + } + + context& context() + { + return m_context; + } + + operator class context&() + { + return m_context; + } + + auto* operator->() + { + return &m_context; + } + + auto& operator*() + { + return m_context; + } + +private: + basic_context_impl m_context; + std::array m_stack{}; +}; + /** * @brief A RAII-style guard for exclusive access to a context * diff --git a/tests/basic_context.test.cpp b/tests/basic_context.test.cpp new file mode 100644 index 0000000..a9919b9 --- /dev/null +++ b/tests/basic_context.test.cpp @@ -0,0 +1,127 @@ +#include + +#include + +import async_context; +import test_utils; + +boost::ut::suite<"basic_context"> basic_context = []() { + using namespace boost::ut; + + static constexpr auto stack_size = 1024; + + "sync return type void"_test = []() { + // Setup + async::basic_context ctx; + + unsigned step = 0; + auto sync_coroutine = [&step](async::context&) -> async::future { + step = 1; + return {}; + }; + + // Exercise + auto future = sync_coroutine(ctx); + + // Verify + expect(that % 0 == ctx->memory_used()); + expect(that % future.done()); + expect(that % future.has_value()); + expect(that % 1 == step); + + expect(that % stack_size == ctx->capacity()); + }; + + "co_await coroutine"_test = []() { + // Setup + async::basic_context ctx; + + static constexpr int expected_return_value = 1413; + unsigned step = 0; + auto co2 = [&step](async::context&) -> async::future { + step = 2; + co_await std::suspend_always{}; + // skipped as the co1 will immediately resume + step = 3; + co_return expected_return_value; + }; + auto co = [&step, &co2](async::context& p_ctx) -> async::future { + step = 1; // skipped as the co2 will immediately start + co_await co2(p_ctx); + step = 4; + co_return expected_return_value; + }; + + // Exercise 1 + auto future = co(ctx); + + // Verify 1 + expect(that % 0 < ctx->memory_used()); + expect(that % not future.done()); + expect(that % not future.has_value()); + expect(that % 0 == step); + + // Exercise 2: start, enter co_2, start immediately and suspend + future.resume(); + + // Verify 2 + expect(that % 0 < ctx->memory_used()); + expect(that % not future.done()); + expect(that % not future.has_value()); + expect(that % 2 == step); + + // Exercise 3: resume, co_2 co_returns, immediately resumes parent, return + future.resume(); + + // Verify 3 + expect(that % 0 == ctx->memory_used()); + expect(that % future.done()); + expect(that % future.has_value()); + expect(that % expected_return_value == future.value()); + expect(that % 4 == step); + + expect(that % stack_size == ctx->capacity()); + }; + + "co_await coroutine"_test = []() { + // Setup + async::basic_context ctx; + + static constexpr int return_value1 = 1413; + static constexpr int return_value2 = 4324; + static constexpr int expected_total = return_value1 + return_value2; + + unsigned step = 0; + auto co2 = [](async::context&) -> async::future { + return return_value1; + }; + + auto co = [&step, &co2](async::context& p_ctx) -> async::future { + step = 1; // skipped as the co2 will immediately start + auto val = co_await co2(p_ctx); + step = 2; + co_return val + return_value2; + }; + + // Exercise 1 + auto future = co(ctx); + + // Verify 1 + expect(that % 0 < ctx->memory_used()); + expect(that % not future.done()); + expect(that % not future.has_value()); + expect(that % 0 == step); + + // Exercise 2: start, call co_2, returns value immediately and co_returns + future.resume(); + + // Verify 3 + expect(that % 0 == ctx->memory_used()); + expect(that % future.done()); + expect(that % future.has_value()); + expect(that % expected_total == future.value()); + expect(that % 2 == step); + + expect(that % stack_size == ctx->capacity()); + }; +};