First
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
add_library(doctest::doctest INTERFACE IMPORTED)
|
||||
target_include_directories(doctest::doctest INTERFACE doctest)
|
||||
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/doctest/doctest.cmake)
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
add_library(tests-main STATIC
|
||||
main.cpp
|
||||
counted.cpp
|
||||
)
|
||||
target_link_libraries(tests-main PUBLIC cppcoro doctest::doctest Threads::Threads)
|
||||
|
||||
set(tests
|
||||
generator_tests.cpp
|
||||
recursive_generator_tests.cpp
|
||||
async_generator_tests.cpp
|
||||
async_auto_reset_event_tests.cpp
|
||||
async_manual_reset_event_tests.cpp
|
||||
async_mutex_tests.cpp
|
||||
async_latch_tests.cpp
|
||||
cancellation_token_tests.cpp
|
||||
task_tests.cpp
|
||||
sequence_barrier_tests.cpp
|
||||
shared_task_tests.cpp
|
||||
sync_wait_tests.cpp
|
||||
single_consumer_async_auto_reset_event_tests.cpp
|
||||
single_producer_sequencer_tests.cpp
|
||||
multi_producer_sequencer_tests.cpp
|
||||
when_all_tests.cpp
|
||||
when_all_ready_tests.cpp
|
||||
ip_address_tests.cpp
|
||||
ip_endpoint_tests.cpp
|
||||
ipv4_address_tests.cpp
|
||||
ipv4_endpoint_tests.cpp
|
||||
ipv6_address_tests.cpp
|
||||
ipv6_endpoint_tests.cpp
|
||||
static_thread_pool_tests.cpp
|
||||
)
|
||||
|
||||
if(WIN32)
|
||||
list(APPEND tests
|
||||
scheduling_operator_tests.cpp
|
||||
io_service_tests.cpp
|
||||
file_tests.cpp
|
||||
socket_tests.cpp
|
||||
)
|
||||
else()
|
||||
# let more time for some tests
|
||||
set(async_auto_reset_event_tests_TIMEOUT 60)
|
||||
endif()
|
||||
|
||||
foreach(test ${tests})
|
||||
get_filename_component(test_name ${test} NAME_WE)
|
||||
add_executable(${test_name} ${test})
|
||||
target_link_libraries(${test_name} PRIVATE tests-main)
|
||||
string(REPLACE "_" " " test_prefix ${test_name})
|
||||
if (NOT DEFINED ${test_name}_TIMEOUT)
|
||||
set(${test_name}_TIMEOUT 30)
|
||||
endif()
|
||||
doctest_discover_tests(${test_name} TEST_PREFIX ${test_prefix}- PROPERTIES TIMEOUT ${${test_name}_TIMEOUT})
|
||||
endforeach()
|
||||
@@ -0,0 +1,140 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/async_auto_reset_event.hpp>
|
||||
|
||||
#include <cppcoro/config.hpp>
|
||||
#include <cppcoro/task.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
#include <cppcoro/when_all.hpp>
|
||||
#include <cppcoro/when_all_ready.hpp>
|
||||
#include <cppcoro/on_scope_exit.hpp>
|
||||
#include <cppcoro/static_thread_pool.hpp>
|
||||
|
||||
#include <thread>
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
|
||||
#include <ostream>
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("async_auto_reset_event");
|
||||
|
||||
TEST_CASE("single waiter")
|
||||
{
|
||||
cppcoro::async_auto_reset_event event;
|
||||
|
||||
bool started = false;
|
||||
bool finished = false;
|
||||
auto run = [&]() -> cppcoro::task<>
|
||||
{
|
||||
started = true;
|
||||
co_await event;
|
||||
finished = true;
|
||||
};
|
||||
|
||||
auto check = [&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(started);
|
||||
CHECK(!finished);
|
||||
|
||||
event.set();
|
||||
|
||||
CHECK(finished);
|
||||
|
||||
co_return;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(run(), check()));
|
||||
}
|
||||
|
||||
TEST_CASE("multiple waiters")
|
||||
{
|
||||
cppcoro::async_auto_reset_event event;
|
||||
|
||||
|
||||
auto run = [&](bool& flag) -> cppcoro::task<>
|
||||
{
|
||||
co_await event;
|
||||
flag = true;
|
||||
};
|
||||
|
||||
bool completed1 = false;
|
||||
bool completed2 = false;
|
||||
|
||||
auto check = [&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(!completed1);
|
||||
CHECK(!completed2);
|
||||
|
||||
event.set();
|
||||
|
||||
CHECK(completed1);
|
||||
CHECK(!completed2);
|
||||
|
||||
event.set();
|
||||
|
||||
CHECK(completed2);
|
||||
|
||||
co_return;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
run(completed1),
|
||||
run(completed2),
|
||||
check()));
|
||||
}
|
||||
|
||||
TEST_CASE("multi-threaded")
|
||||
{
|
||||
cppcoro::static_thread_pool tp{ 3 };
|
||||
|
||||
auto run = [&]() -> cppcoro::task<>
|
||||
{
|
||||
cppcoro::async_auto_reset_event event;
|
||||
|
||||
int value = 0;
|
||||
|
||||
auto startWaiter = [&]() -> cppcoro::task<>
|
||||
{
|
||||
co_await tp.schedule();
|
||||
co_await event;
|
||||
++value;
|
||||
event.set();
|
||||
};
|
||||
|
||||
auto startSignaller = [&]() -> cppcoro::task<>
|
||||
{
|
||||
co_await tp.schedule();
|
||||
value = 5;
|
||||
event.set();
|
||||
};
|
||||
|
||||
std::vector<cppcoro::task<>> tasks;
|
||||
|
||||
tasks.emplace_back(startSignaller());
|
||||
|
||||
for (int i = 0; i < 1000; ++i)
|
||||
{
|
||||
tasks.emplace_back(startWaiter());
|
||||
}
|
||||
|
||||
co_await cppcoro::when_all(std::move(tasks));
|
||||
|
||||
// NOTE: Can't use CHECK() here because it's not thread-safe
|
||||
assert(value == 1005);
|
||||
};
|
||||
|
||||
std::vector<cppcoro::task<>> tasks;
|
||||
|
||||
for (int i = 0; i < 1000; ++i)
|
||||
{
|
||||
tasks.emplace_back(run());
|
||||
}
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all(std::move(tasks)));
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,330 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/async_generator.hpp>
|
||||
#include <cppcoro/single_consumer_event.hpp>
|
||||
#include <cppcoro/task.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
#include <cppcoro/when_all.hpp>
|
||||
|
||||
#include <ostream>
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("async_generator");
|
||||
|
||||
TEST_CASE("default-constructed async_generator is an empty sequence")
|
||||
{
|
||||
cppcoro::sync_wait([]() -> cppcoro::task<>
|
||||
{
|
||||
// Iterating over default-constructed async_generator just
|
||||
// gives an empty sequence.
|
||||
cppcoro::async_generator<int> g;
|
||||
auto it = co_await g.begin();
|
||||
CHECK(it == g.end());
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_CASE("async_generator doesn't start if begin() not called")
|
||||
{
|
||||
bool startedExecution = false;
|
||||
{
|
||||
auto gen = [&]() -> cppcoro::async_generator<int>
|
||||
{
|
||||
startedExecution = true;
|
||||
co_yield 1;
|
||||
}();
|
||||
CHECK(!startedExecution);
|
||||
}
|
||||
CHECK(!startedExecution);
|
||||
}
|
||||
|
||||
TEST_CASE("enumerate sequence of 1 value")
|
||||
{
|
||||
cppcoro::sync_wait([]() -> cppcoro::task<>
|
||||
{
|
||||
bool startedExecution = false;
|
||||
auto makeGenerator = [&]() -> cppcoro::async_generator<std::uint32_t>
|
||||
{
|
||||
startedExecution = true;
|
||||
co_yield 1;
|
||||
};
|
||||
|
||||
auto gen = makeGenerator();
|
||||
|
||||
CHECK(!startedExecution);
|
||||
|
||||
auto it = co_await gen.begin();
|
||||
|
||||
CHECK(startedExecution);
|
||||
CHECK(it != gen.end());
|
||||
CHECK(*it == 1u);
|
||||
CHECK(co_await ++it == gen.end());
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_CASE("enumerate sequence of multiple values")
|
||||
{
|
||||
cppcoro::sync_wait([]() -> cppcoro::task<>
|
||||
{
|
||||
bool startedExecution = false;
|
||||
auto makeGenerator = [&]() -> cppcoro::async_generator<std::uint32_t>
|
||||
{
|
||||
startedExecution = true;
|
||||
co_yield 1;
|
||||
co_yield 2;
|
||||
co_yield 3;
|
||||
};
|
||||
|
||||
auto gen = makeGenerator();
|
||||
|
||||
CHECK(!startedExecution);
|
||||
|
||||
auto it = co_await gen.begin();
|
||||
|
||||
CHECK(startedExecution);
|
||||
|
||||
CHECK(it != gen.end());
|
||||
CHECK(*it == 1u);
|
||||
|
||||
CHECK(co_await ++it != gen.end());
|
||||
CHECK(*it == 2u);
|
||||
|
||||
CHECK(co_await ++it != gen.end());
|
||||
CHECK(*it == 3u);
|
||||
|
||||
CHECK(co_await ++it == gen.end());
|
||||
}());
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
class set_to_true_on_destruction
|
||||
{
|
||||
public:
|
||||
|
||||
set_to_true_on_destruction(bool* value)
|
||||
: m_value(value)
|
||||
{}
|
||||
|
||||
set_to_true_on_destruction(set_to_true_on_destruction&& other)
|
||||
: m_value(other.m_value)
|
||||
{
|
||||
other.m_value = nullptr;
|
||||
}
|
||||
|
||||
~set_to_true_on_destruction()
|
||||
{
|
||||
if (m_value != nullptr)
|
||||
{
|
||||
*m_value = true;
|
||||
}
|
||||
}
|
||||
|
||||
set_to_true_on_destruction(const set_to_true_on_destruction&) = delete;
|
||||
set_to_true_on_destruction& operator=(const set_to_true_on_destruction&) = delete;
|
||||
|
||||
private:
|
||||
|
||||
bool* m_value;
|
||||
};
|
||||
}
|
||||
|
||||
TEST_CASE("destructors of values in scope are called when async_generator destructed early")
|
||||
{
|
||||
cppcoro::sync_wait([]() -> cppcoro::task<>
|
||||
{
|
||||
bool aDestructed = false;
|
||||
bool bDestructed = false;
|
||||
|
||||
auto makeGenerator = [&](set_to_true_on_destruction a) -> cppcoro::async_generator<std::uint32_t>
|
||||
{
|
||||
set_to_true_on_destruction b(&bDestructed);
|
||||
co_yield 1;
|
||||
co_yield 2;
|
||||
};
|
||||
|
||||
{
|
||||
auto gen = makeGenerator(&aDestructed);
|
||||
|
||||
CHECK(!aDestructed);
|
||||
CHECK(!bDestructed);
|
||||
|
||||
auto it = co_await gen.begin();
|
||||
CHECK(!aDestructed);
|
||||
CHECK(!bDestructed);
|
||||
CHECK(*it == 1u);
|
||||
}
|
||||
|
||||
CHECK(aDestructed);
|
||||
CHECK(bDestructed);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_CASE("async producer with async consumer"
|
||||
* doctest::description{
|
||||
"This test tries to cover the different state-transition code-paths\n"
|
||||
"- consumer resuming producer and producer completing asynchronously\n"
|
||||
"- producer resuming consumer and consumer requesting next value synchronously\n"
|
||||
"- producer resuming consumer and consumer requesting next value asynchronously" })
|
||||
{
|
||||
#if defined(_MSC_VER) && _MSC_FULL_VER <= 191025224 && defined(CPPCORO_RELEASE_OPTIMISED)
|
||||
FAST_WARN_UNARY_FALSE("MSVC has a known codegen bug under optimised builds, skipping");
|
||||
return;
|
||||
#endif
|
||||
|
||||
cppcoro::single_consumer_event p1;
|
||||
cppcoro::single_consumer_event p2;
|
||||
cppcoro::single_consumer_event p3;
|
||||
cppcoro::single_consumer_event c1;
|
||||
|
||||
auto produce = [&]() -> cppcoro::async_generator<std::uint32_t>
|
||||
{
|
||||
co_await p1;
|
||||
co_yield 1;
|
||||
co_await p2;
|
||||
co_yield 2;
|
||||
co_await p3;
|
||||
};
|
||||
|
||||
bool consumerFinished = false;
|
||||
|
||||
auto consume = [&]() -> cppcoro::task<>
|
||||
{
|
||||
auto generator = produce();
|
||||
auto it = co_await generator.begin();
|
||||
CHECK(*it == 1u);
|
||||
(void)co_await ++it;
|
||||
CHECK(*it == 2u);
|
||||
co_await c1;
|
||||
(void)co_await ++it;
|
||||
CHECK(it == generator.end());
|
||||
consumerFinished = true;
|
||||
};
|
||||
|
||||
auto unblock = [&]() -> cppcoro::task<>
|
||||
{
|
||||
p1.set();
|
||||
p2.set();
|
||||
c1.set();
|
||||
CHECK(!consumerFinished);
|
||||
p3.set();
|
||||
CHECK(consumerFinished);
|
||||
co_return;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(consume(), unblock()));
|
||||
}
|
||||
|
||||
TEST_CASE("exception thrown before first yield is rethrown from begin operation")
|
||||
{
|
||||
class TestException {};
|
||||
auto gen = [](bool shouldThrow) -> cppcoro::async_generator<std::uint32_t>
|
||||
{
|
||||
if (shouldThrow)
|
||||
{
|
||||
throw TestException();
|
||||
}
|
||||
co_yield 1;
|
||||
}(true);
|
||||
|
||||
cppcoro::sync_wait([&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK_THROWS_AS(co_await gen.begin(), const TestException&);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_CASE("exception thrown after first yield is rethrown from increment operator")
|
||||
{
|
||||
class TestException {};
|
||||
auto gen = [](bool shouldThrow) -> cppcoro::async_generator<std::uint32_t>
|
||||
{
|
||||
co_yield 1;
|
||||
if (shouldThrow)
|
||||
{
|
||||
throw TestException();
|
||||
}
|
||||
}(true);
|
||||
|
||||
cppcoro::sync_wait([&]() -> cppcoro::task<>
|
||||
{
|
||||
auto it = co_await gen.begin();
|
||||
CHECK(*it == 1u);
|
||||
CHECK_THROWS_AS(co_await ++it, const TestException&);
|
||||
CHECK(it == gen.end());
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_CASE("large number of synchronous completions doesn't result in stack-overflow")
|
||||
{
|
||||
|
||||
auto makeSequence = [](cppcoro::single_consumer_event& event) -> cppcoro::async_generator<std::uint32_t>
|
||||
{
|
||||
for (std::uint32_t i = 0; i < 1'000'000u; ++i)
|
||||
{
|
||||
if (i == 500'000u) co_await event;
|
||||
co_yield i;
|
||||
}
|
||||
};
|
||||
|
||||
auto consumer = [](cppcoro::async_generator<std::uint32_t> sequence) -> cppcoro::task<>
|
||||
{
|
||||
std::uint32_t expected = 0;
|
||||
for (auto iter = co_await sequence.begin(); iter != sequence.end(); co_await ++iter)
|
||||
{
|
||||
std::uint32_t i = *iter;
|
||||
CHECK(i == expected++);
|
||||
}
|
||||
|
||||
CHECK(expected == 1'000'000u);
|
||||
};
|
||||
|
||||
auto unblocker = [](cppcoro::single_consumer_event& event) -> cppcoro::task<>
|
||||
{
|
||||
// Should have processed the first 500'000 elements synchronously with consumer driving
|
||||
// iteraction before producer suspends and thus consumer suspends.
|
||||
// Then we resume producer in call to set() below and it continues processing remaining
|
||||
// 500'000 elements, this time with producer driving the interaction.
|
||||
|
||||
event.set();
|
||||
|
||||
co_return;
|
||||
};
|
||||
|
||||
cppcoro::single_consumer_event event;
|
||||
|
||||
cppcoro::sync_wait(
|
||||
cppcoro::when_all_ready(
|
||||
consumer(makeSequence(event)),
|
||||
unblocker(event)));
|
||||
}
|
||||
|
||||
TEST_CASE("fmap")
|
||||
{
|
||||
using cppcoro::async_generator;
|
||||
using cppcoro::fmap;
|
||||
|
||||
auto iota = [](int count) -> async_generator<int>
|
||||
{
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
co_yield i;
|
||||
}
|
||||
};
|
||||
|
||||
auto squares = iota(5) | fmap([](auto x) { return x * x; });
|
||||
|
||||
cppcoro::sync_wait([&]() -> cppcoro::task<>
|
||||
{
|
||||
auto it = co_await squares.begin();
|
||||
CHECK(*it == 0);
|
||||
CHECK(*co_await ++it == 1);
|
||||
CHECK(*co_await ++it == 4);
|
||||
CHECK(*co_await ++it == 9);
|
||||
CHECK(*co_await ++it == 16);
|
||||
CHECK(co_await ++it == squares.end());
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,113 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/async_latch.hpp>
|
||||
#include <cppcoro/single_consumer_event.hpp>
|
||||
#include <cppcoro/task.hpp>
|
||||
#include <cppcoro/when_all_ready.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
|
||||
#include <ostream>
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("async_latch");
|
||||
|
||||
using namespace cppcoro;
|
||||
|
||||
TEST_CASE("latch constructed with zero count is initially ready")
|
||||
{
|
||||
async_latch latch(0);
|
||||
CHECK(latch.is_ready());
|
||||
}
|
||||
|
||||
TEST_CASE("latch constructed with negative count is initially ready")
|
||||
{
|
||||
async_latch latch(-3);
|
||||
CHECK(latch.is_ready());
|
||||
}
|
||||
|
||||
TEST_CASE("count_down and is_ready")
|
||||
{
|
||||
async_latch latch(3);
|
||||
CHECK(!latch.is_ready());
|
||||
latch.count_down();
|
||||
CHECK(!latch.is_ready());
|
||||
latch.count_down();
|
||||
CHECK(!latch.is_ready());
|
||||
latch.count_down();
|
||||
CHECK(latch.is_ready());
|
||||
}
|
||||
|
||||
TEST_CASE("count_down by n")
|
||||
{
|
||||
async_latch latch(5);
|
||||
latch.count_down(3);
|
||||
CHECK(!latch.is_ready());
|
||||
latch.count_down(2);
|
||||
CHECK(latch.is_ready());
|
||||
}
|
||||
|
||||
TEST_CASE("single awaiter")
|
||||
{
|
||||
async_latch latch(2);
|
||||
bool after = false;
|
||||
sync_wait(when_all_ready(
|
||||
[&]() -> task<>
|
||||
{
|
||||
co_await latch;
|
||||
after = true;
|
||||
}(),
|
||||
[&]() -> task<>
|
||||
{
|
||||
CHECK(!after);
|
||||
latch.count_down();
|
||||
CHECK(!after);
|
||||
latch.count_down();
|
||||
CHECK(after);
|
||||
co_return;
|
||||
}()
|
||||
));
|
||||
}
|
||||
|
||||
TEST_CASE("multiple awaiters")
|
||||
{
|
||||
async_latch latch(2);
|
||||
bool after1 = false;
|
||||
bool after2 = false;
|
||||
bool after3 = false;
|
||||
sync_wait(when_all_ready(
|
||||
[&]() -> task<>
|
||||
{
|
||||
co_await latch;
|
||||
after1 = true;
|
||||
}(),
|
||||
[&]() -> task<>
|
||||
{
|
||||
co_await latch;
|
||||
after2 = true;
|
||||
}(),
|
||||
[&]() -> task<>
|
||||
{
|
||||
co_await latch;
|
||||
after3 = true;
|
||||
}(),
|
||||
[&]() -> task<>
|
||||
{
|
||||
CHECK(!after1);
|
||||
CHECK(!after2);
|
||||
CHECK(!after3);
|
||||
latch.count_down();
|
||||
CHECK(!after1);
|
||||
CHECK(!after2);
|
||||
CHECK(!after3);
|
||||
latch.count_down();
|
||||
CHECK(after1);
|
||||
CHECK(after2);
|
||||
CHECK(after3);
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,96 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/async_manual_reset_event.hpp>
|
||||
|
||||
#include <cppcoro/task.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
#include <cppcoro/when_all_ready.hpp>
|
||||
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("async_manual_reset_event");
|
||||
|
||||
TEST_CASE("default constructor initially not set")
|
||||
{
|
||||
cppcoro::async_manual_reset_event event;
|
||||
CHECK(!event.is_set());
|
||||
}
|
||||
|
||||
TEST_CASE("construct event initially set")
|
||||
{
|
||||
cppcoro::async_manual_reset_event event{ true };
|
||||
CHECK(event.is_set());
|
||||
}
|
||||
|
||||
TEST_CASE("set and reset")
|
||||
{
|
||||
cppcoro::async_manual_reset_event event;
|
||||
CHECK(!event.is_set());
|
||||
event.set();
|
||||
CHECK(event.is_set());
|
||||
event.set();
|
||||
CHECK(event.is_set());
|
||||
event.reset();
|
||||
CHECK(!event.is_set());
|
||||
event.reset();
|
||||
CHECK(!event.is_set());
|
||||
event.set();
|
||||
CHECK(event.is_set());
|
||||
}
|
||||
|
||||
TEST_CASE("await not set event")
|
||||
{
|
||||
cppcoro::async_manual_reset_event event;
|
||||
|
||||
auto createWaiter = [&](bool& flag) -> cppcoro::task<>
|
||||
{
|
||||
co_await event;
|
||||
flag = true;
|
||||
};
|
||||
|
||||
bool completed1 = false;
|
||||
bool completed2 = false;
|
||||
|
||||
auto check = [&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(!completed1);
|
||||
CHECK(!completed2);
|
||||
|
||||
event.reset();
|
||||
|
||||
CHECK(!completed1);
|
||||
CHECK(!completed2);
|
||||
|
||||
event.set();
|
||||
|
||||
CHECK(completed1);
|
||||
CHECK(completed2);
|
||||
|
||||
co_return;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
createWaiter(completed1),
|
||||
createWaiter(completed2),
|
||||
check()));
|
||||
}
|
||||
|
||||
TEST_CASE("awaiting already set event doesn't suspend")
|
||||
{
|
||||
cppcoro::async_manual_reset_event event{ true };
|
||||
|
||||
auto createWaiter = [&]() -> cppcoro::task<>
|
||||
{
|
||||
co_await event;
|
||||
};
|
||||
|
||||
// Should complete without blocking.
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
createWaiter(),
|
||||
createWaiter()));
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,90 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/async_mutex.hpp>
|
||||
#include <cppcoro/single_consumer_event.hpp>
|
||||
#include <cppcoro/task.hpp>
|
||||
#include <cppcoro/when_all_ready.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
|
||||
#include <ostream>
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("async_mutex");
|
||||
|
||||
TEST_CASE("try_lock")
|
||||
{
|
||||
cppcoro::async_mutex mutex;
|
||||
|
||||
CHECK(mutex.try_lock());
|
||||
|
||||
CHECK_FALSE(mutex.try_lock());
|
||||
|
||||
mutex.unlock();
|
||||
|
||||
CHECK(mutex.try_lock());
|
||||
}
|
||||
|
||||
#if 0
|
||||
TEST_CASE("multiple lockers")
|
||||
{
|
||||
int value = 0;
|
||||
cppcoro::async_mutex mutex;
|
||||
cppcoro::single_consumer_event a;
|
||||
cppcoro::single_consumer_event b;
|
||||
cppcoro::single_consumer_event c;
|
||||
cppcoro::single_consumer_event d;
|
||||
|
||||
auto f = [&](cppcoro::single_consumer_event& e) -> cppcoro::task<>
|
||||
{
|
||||
auto lock = co_await mutex.scoped_lock_async();
|
||||
co_await e;
|
||||
++value;
|
||||
};
|
||||
|
||||
auto check = [&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(value == 0);
|
||||
|
||||
a.set();
|
||||
|
||||
CHECK(value == 1);
|
||||
|
||||
auto check2 = [&]() -> cppcoro::task<>
|
||||
{
|
||||
b.set();
|
||||
|
||||
CHECK(value == 2);
|
||||
|
||||
c.set();
|
||||
|
||||
CHECK(value == 3);
|
||||
|
||||
d.set();
|
||||
|
||||
CHECK(value == 4);
|
||||
|
||||
co_return;
|
||||
};
|
||||
|
||||
// Now that we've queued some waiters and released one waiter this will
|
||||
// have acquired the list of pending waiters in the local cache.
|
||||
// We'll now queue up another one before releasing any more waiters
|
||||
// to test the code-path that looks at the newly queued waiter list
|
||||
// when the cache of waiters is exhausted.
|
||||
(void)co_await cppcoro::when_all_ready(f(d), check2());
|
||||
};
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
f(a),
|
||||
f(b),
|
||||
f(c),
|
||||
check()));
|
||||
|
||||
CHECK(value == 4);
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,93 @@
|
||||
###############################################################################
|
||||
# Copyright (c) Lewis Baker
|
||||
# Licenced under MIT license. See LICENSE.txt for details.
|
||||
###############################################################################
|
||||
|
||||
import cake.path
|
||||
|
||||
from cake.tools import script, env, compiler, project, variant, test
|
||||
|
||||
script.include([
|
||||
env.expand('${CPPCORO}/lib/use.cake'),
|
||||
])
|
||||
|
||||
headers = script.cwd([
|
||||
"counted.hpp",
|
||||
"io_service_fixture.hpp",
|
||||
])
|
||||
|
||||
sources = script.cwd([
|
||||
'main.cpp',
|
||||
'counted.cpp',
|
||||
'generator_tests.cpp',
|
||||
'recursive_generator_tests.cpp',
|
||||
'async_generator_tests.cpp',
|
||||
'async_auto_reset_event_tests.cpp',
|
||||
'async_manual_reset_event_tests.cpp',
|
||||
'async_mutex_tests.cpp',
|
||||
'async_latch_tests.cpp',
|
||||
'cancellation_token_tests.cpp',
|
||||
'task_tests.cpp',
|
||||
'sequence_barrier_tests.cpp',
|
||||
'shared_task_tests.cpp',
|
||||
'sync_wait_tests.cpp',
|
||||
'single_consumer_async_auto_reset_event_tests.cpp',
|
||||
'single_producer_sequencer_tests.cpp',
|
||||
'multi_producer_sequencer_tests.cpp',
|
||||
'when_all_tests.cpp',
|
||||
'when_all_ready_tests.cpp',
|
||||
'ip_address_tests.cpp',
|
||||
'ip_endpoint_tests.cpp',
|
||||
'ipv4_address_tests.cpp',
|
||||
'ipv4_endpoint_tests.cpp',
|
||||
'ipv6_address_tests.cpp',
|
||||
'ipv6_endpoint_tests.cpp',
|
||||
'static_thread_pool_tests.cpp',
|
||||
])
|
||||
|
||||
if variant.platform == 'windows':
|
||||
sources += script.cwd([
|
||||
'scheduling_operator_tests.cpp',
|
||||
'io_service_tests.cpp',
|
||||
'file_tests.cpp',
|
||||
'socket_tests.cpp',
|
||||
])
|
||||
|
||||
extras = script.cwd([
|
||||
'build.cake',
|
||||
])
|
||||
|
||||
intermediateBuildDir = cake.path.join(env.expand('${CPPCORO_BUILD}'), 'test', 'obj')
|
||||
|
||||
compiler.addDefine('CPPCORO_RELEASE_' + variant.release.upper())
|
||||
|
||||
objects = compiler.objects(
|
||||
targetDir=intermediateBuildDir,
|
||||
sources=sources,
|
||||
)
|
||||
|
||||
testExe = compiler.program(
|
||||
target=env.expand('${CPPCORO_BUILD}/test/run'),
|
||||
sources=objects,
|
||||
)
|
||||
|
||||
test.alwaysRun = True
|
||||
testResult = test.run(
|
||||
program=testExe,
|
||||
results=env.expand('${CPPCORO_BUILD}/test/run.results'),
|
||||
)
|
||||
script.addTarget('testresult', testResult)
|
||||
|
||||
vcproj = project.project(
|
||||
target=env.expand('${CPPCORO_PROJECT}/cppcoro_tests'),
|
||||
items={
|
||||
'Source': sources + headers,
|
||||
'': extras,
|
||||
},
|
||||
output=testExe,
|
||||
)
|
||||
|
||||
script.setResult(
|
||||
project=vcproj,
|
||||
test=testExe,
|
||||
)
|
||||
@@ -0,0 +1,342 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/cancellation_token.hpp>
|
||||
#include <cppcoro/cancellation_source.hpp>
|
||||
#include <cppcoro/cancellation_registration.hpp>
|
||||
#include <cppcoro/operation_cancelled.hpp>
|
||||
|
||||
#include <thread>
|
||||
|
||||
#include <ostream>
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("cancellation_token tests");
|
||||
|
||||
TEST_CASE("default cancellation_token is not cancellable")
|
||||
{
|
||||
cppcoro::cancellation_token t;
|
||||
CHECK(!t.is_cancellation_requested());
|
||||
CHECK(!t.can_be_cancelled());
|
||||
}
|
||||
|
||||
TEST_CASE("calling request_cancellation on cancellation_source updates cancellation_token")
|
||||
{
|
||||
cppcoro::cancellation_source s;
|
||||
cppcoro::cancellation_token t = s.token();
|
||||
CHECK(t.can_be_cancelled());
|
||||
CHECK(!t.is_cancellation_requested());
|
||||
s.request_cancellation();
|
||||
CHECK(t.is_cancellation_requested());
|
||||
CHECK(t.can_be_cancelled());
|
||||
}
|
||||
|
||||
TEST_CASE("cancellation_token can't be cancelled when last cancellation_source destructed")
|
||||
{
|
||||
cppcoro::cancellation_token t;
|
||||
{
|
||||
cppcoro::cancellation_source s;
|
||||
t = s.token();
|
||||
CHECK(t.can_be_cancelled());
|
||||
}
|
||||
|
||||
CHECK(!t.can_be_cancelled());
|
||||
}
|
||||
|
||||
TEST_CASE("cancelation_token can be cancelled when last cancellation_source destructed if cancellation already requested")
|
||||
{
|
||||
cppcoro::cancellation_token t;
|
||||
{
|
||||
cppcoro::cancellation_source s;
|
||||
t = s.token();
|
||||
CHECK(t.can_be_cancelled());
|
||||
s.request_cancellation();
|
||||
}
|
||||
|
||||
CHECK(t.can_be_cancelled());
|
||||
CHECK(t.is_cancellation_requested());
|
||||
}
|
||||
|
||||
TEST_CASE("cancellation_registration when cancellation not yet requested")
|
||||
{
|
||||
cppcoro::cancellation_source s;
|
||||
|
||||
bool callbackExecuted = false;
|
||||
{
|
||||
cppcoro::cancellation_registration callbackRegistration(
|
||||
s.token(),
|
||||
[&] { callbackExecuted = true; });
|
||||
}
|
||||
|
||||
CHECK(!callbackExecuted);
|
||||
|
||||
{
|
||||
cppcoro::cancellation_registration callbackRegistration(
|
||||
s.token(),
|
||||
[&] { callbackExecuted = true; });
|
||||
|
||||
CHECK(!callbackExecuted);
|
||||
|
||||
s.request_cancellation();
|
||||
|
||||
CHECK(callbackExecuted);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("throw_if_cancellation_requested")
|
||||
{
|
||||
cppcoro::cancellation_source s;
|
||||
cppcoro::cancellation_token t = s.token();
|
||||
|
||||
CHECK_NOTHROW(t.throw_if_cancellation_requested());
|
||||
|
||||
s.request_cancellation();
|
||||
|
||||
CHECK_THROWS_AS(t.throw_if_cancellation_requested(), const cppcoro::operation_cancelled&);
|
||||
}
|
||||
|
||||
TEST_CASE("cancellation_registration called immediately when cancellation already requested")
|
||||
{
|
||||
cppcoro::cancellation_source s;
|
||||
s.request_cancellation();
|
||||
|
||||
bool executed = false;
|
||||
cppcoro::cancellation_registration r{ s.token(), [&] { executed = true; } };
|
||||
CHECK(executed);
|
||||
}
|
||||
|
||||
TEST_CASE("register many callbacks"
|
||||
* doctest::description{
|
||||
"this checks the code-path that allocates the next chunk of entries "
|
||||
"in the internal data-structres, which occurs on 17th callback" })
|
||||
{
|
||||
cppcoro::cancellation_source s;
|
||||
auto t = s.token();
|
||||
|
||||
int callbackExecutionCount = 0;
|
||||
auto callback = [&] { ++callbackExecutionCount; };
|
||||
|
||||
// Allocate enough to require a second chunk to be allocated.
|
||||
cppcoro::cancellation_registration r1{ t, callback };
|
||||
cppcoro::cancellation_registration r2{ t, callback };
|
||||
cppcoro::cancellation_registration r3{ t, callback };
|
||||
cppcoro::cancellation_registration r4{ t, callback };
|
||||
cppcoro::cancellation_registration r5{ t, callback };
|
||||
cppcoro::cancellation_registration r6{ t, callback };
|
||||
cppcoro::cancellation_registration r7{ t, callback };
|
||||
cppcoro::cancellation_registration r8{ t, callback };
|
||||
cppcoro::cancellation_registration r9{ t, callback };
|
||||
cppcoro::cancellation_registration r10{ t, callback };
|
||||
cppcoro::cancellation_registration r11{ t, callback };
|
||||
cppcoro::cancellation_registration r12{ t, callback };
|
||||
cppcoro::cancellation_registration r13{ t, callback };
|
||||
cppcoro::cancellation_registration r14{ t, callback };
|
||||
cppcoro::cancellation_registration r15{ t, callback };
|
||||
cppcoro::cancellation_registration r16{ t, callback };
|
||||
cppcoro::cancellation_registration r17{ t, callback };
|
||||
cppcoro::cancellation_registration r18{ t, callback };
|
||||
|
||||
s.request_cancellation();
|
||||
|
||||
CHECK(callbackExecutionCount == 18);
|
||||
}
|
||||
|
||||
TEST_CASE("concurrent registration and cancellation")
|
||||
{
|
||||
// Just check this runs and terminates without crashing.
|
||||
for (int i = 0; i < 100; ++i)
|
||||
{
|
||||
cppcoro::cancellation_source source;
|
||||
|
||||
std::thread waiter1{ [token = source.token()]
|
||||
{
|
||||
std::atomic<bool> cancelled = false;
|
||||
while (!cancelled)
|
||||
{
|
||||
cppcoro::cancellation_registration registration{ token, [&]
|
||||
{
|
||||
cancelled = true;
|
||||
} };
|
||||
|
||||
cppcoro::cancellation_registration reg0{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg1{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg2{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg3{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg4{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg5{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg6{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg7{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg8{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg9{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg10{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg11{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg12{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg13{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg14{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg15{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg17{ token, [] {} };
|
||||
|
||||
std::this_thread::yield();
|
||||
}
|
||||
} };
|
||||
|
||||
std::thread waiter2{ [token = source.token()]
|
||||
{
|
||||
std::atomic<bool> cancelled = false;
|
||||
while (!cancelled)
|
||||
{
|
||||
cppcoro::cancellation_registration registration{ token, [&]
|
||||
{
|
||||
cancelled = true;
|
||||
} };
|
||||
|
||||
cppcoro::cancellation_registration reg0{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg1{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg2{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg3{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg4{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg5{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg6{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg7{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg8{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg9{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg10{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg11{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg12{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg13{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg14{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg15{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg16{ token, [] {} };
|
||||
|
||||
std::this_thread::yield();
|
||||
}
|
||||
} };
|
||||
|
||||
std::thread waiter3{ [token = source.token()]
|
||||
{
|
||||
std::atomic<bool> cancelled = false;
|
||||
while (!cancelled)
|
||||
{
|
||||
cppcoro::cancellation_registration registration{ token, [&]
|
||||
{
|
||||
cancelled = true;
|
||||
} };
|
||||
|
||||
cppcoro::cancellation_registration reg0{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg1{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg2{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg3{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg4{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg5{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg6{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg7{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg8{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg9{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg10{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg11{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg12{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg13{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg14{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg15{ token, [] {} };
|
||||
cppcoro::cancellation_registration reg16{ token, [] {} };
|
||||
|
||||
std::this_thread::yield();
|
||||
}
|
||||
} };
|
||||
|
||||
std::thread canceller{ [&source]
|
||||
{
|
||||
source.request_cancellation();
|
||||
} };
|
||||
|
||||
canceller.join();
|
||||
waiter1.join();
|
||||
waiter2.join();
|
||||
waiter3.join();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("cancellation registration single-threaded performance")
|
||||
{
|
||||
struct batch
|
||||
{
|
||||
batch(cppcoro::cancellation_token t)
|
||||
: r0(t, [] {})
|
||||
, r1(t, [] {})
|
||||
, r2(t, [] {})
|
||||
, r3(t, [] {})
|
||||
, r4(t, [] {})
|
||||
, r5(t, [] {})
|
||||
, r6(t, [] {})
|
||||
, r7(t, [] {})
|
||||
, r8(t, [] {})
|
||||
, r9(t, [] {})
|
||||
{}
|
||||
|
||||
cppcoro::cancellation_registration r0;
|
||||
cppcoro::cancellation_registration r1;
|
||||
cppcoro::cancellation_registration r2;
|
||||
cppcoro::cancellation_registration r3;
|
||||
cppcoro::cancellation_registration r4;
|
||||
cppcoro::cancellation_registration r5;
|
||||
cppcoro::cancellation_registration r6;
|
||||
cppcoro::cancellation_registration r7;
|
||||
cppcoro::cancellation_registration r8;
|
||||
cppcoro::cancellation_registration r9;
|
||||
};
|
||||
|
||||
cppcoro::cancellation_source s;
|
||||
|
||||
constexpr int iterationCount = 100'000;
|
||||
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
for (int i = 0; i < iterationCount; ++i)
|
||||
{
|
||||
cppcoro::cancellation_registration r{ s.token(), [] {} };
|
||||
}
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
|
||||
auto time1 = end - start;
|
||||
|
||||
start = end;
|
||||
|
||||
for (int i = 0; i < iterationCount; ++i)
|
||||
{
|
||||
batch b{ s.token() };
|
||||
}
|
||||
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
|
||||
auto time2 = end - start;
|
||||
|
||||
start = end;
|
||||
|
||||
for (int i = 0; i < iterationCount; ++i)
|
||||
{
|
||||
batch b0{ s.token() };
|
||||
batch b1{ s.token() };
|
||||
batch b2{ s.token() };
|
||||
batch b3{ s.token() };
|
||||
batch b4{ s.token() };
|
||||
}
|
||||
|
||||
end = std::chrono::high_resolution_clock::now();
|
||||
|
||||
auto time3 = end - start;
|
||||
|
||||
auto report = [](const char* label, auto time, std::uint64_t count)
|
||||
{
|
||||
auto us = std::chrono::duration_cast<std::chrono::microseconds>(time).count();
|
||||
MESSAGE(label << " took " << us << "us (" << (1000.0 * us / count) << " ns/item)");
|
||||
};
|
||||
|
||||
report("Individual", time1, iterationCount);
|
||||
report("Batch10", time2, 10 * iterationCount);
|
||||
report("Batch50", time3, 50 * iterationCount);
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,11 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "counted.hpp"
|
||||
|
||||
int counted::default_construction_count;
|
||||
int counted::copy_construction_count;
|
||||
int counted::move_construction_count;
|
||||
int counted::destruction_count;
|
||||
@@ -0,0 +1,42 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
#ifndef CPPCORO_TESTS_COUNTED_HPP_INCLUDED
|
||||
#define CPPCORO_TESTS_COUNTED_HPP_INCLUDED
|
||||
|
||||
struct counted
|
||||
{
|
||||
static int default_construction_count;
|
||||
static int copy_construction_count;
|
||||
static int move_construction_count;
|
||||
static int destruction_count;
|
||||
|
||||
int id;
|
||||
|
||||
static void reset_counts()
|
||||
{
|
||||
default_construction_count = 0;
|
||||
copy_construction_count = 0;
|
||||
move_construction_count = 0;
|
||||
destruction_count = 0;
|
||||
}
|
||||
|
||||
static int construction_count()
|
||||
{
|
||||
return default_construction_count + copy_construction_count + move_construction_count;
|
||||
}
|
||||
|
||||
static int active_count()
|
||||
{
|
||||
return construction_count() - destruction_count;
|
||||
}
|
||||
|
||||
counted() : id(default_construction_count++) {}
|
||||
counted(const counted& other) : id(other.id) { ++copy_construction_count; }
|
||||
counted(counted&& other) : id(other.id) { ++move_construction_count; other.id = -1; }
|
||||
~counted() { ++destruction_count; }
|
||||
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,12 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Andreas Buhr
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
#ifndef CPPCORO_CPPCORO_DOCTEST_H_INCLUDED
|
||||
#define CPPCORO_CPPCORO_DOCTEST_H_INCLUDED
|
||||
|
||||
#define DOCTEST_CONFIG_USE_STD_HEADERS
|
||||
#include "doctest.h"
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
||||
# file Copyright.txt or https://cmake.org/licensing for details.
|
||||
|
||||
#[=======================================================================[.rst:
|
||||
doctest
|
||||
-----
|
||||
|
||||
This module defines a function to help use the doctest test framework.
|
||||
|
||||
The :command:`doctest_discover_tests` discovers tests by asking the compiled test
|
||||
executable to enumerate its tests. This does not require CMake to be re-run
|
||||
when tests change. However, it may not work in a cross-compiling environment,
|
||||
and setting test properties is less convenient.
|
||||
|
||||
This command is intended to replace use of :command:`add_test` to register
|
||||
tests, and will create a separate CTest test for each doctest test case. Note
|
||||
that this is in some cases less efficient, as common set-up and tear-down logic
|
||||
cannot be shared by multiple test cases executing in the same instance.
|
||||
However, it provides more fine-grained pass/fail information to CTest, which is
|
||||
usually considered as more beneficial. By default, the CTest test name is the
|
||||
same as the doctest name; see also ``TEST_PREFIX`` and ``TEST_SUFFIX``.
|
||||
|
||||
.. command:: doctest_discover_tests
|
||||
|
||||
Automatically add tests with CTest by querying the compiled test executable
|
||||
for available tests::
|
||||
|
||||
doctest_discover_tests(target
|
||||
[TEST_SPEC arg1...]
|
||||
[EXTRA_ARGS arg1...]
|
||||
[WORKING_DIRECTORY dir]
|
||||
[TEST_PREFIX prefix]
|
||||
[TEST_SUFFIX suffix]
|
||||
[PROPERTIES name1 value1...]
|
||||
[TEST_LIST var]
|
||||
)
|
||||
|
||||
``doctest_discover_tests`` sets up a post-build command on the test executable
|
||||
that generates the list of tests by parsing the output from running the test
|
||||
with the ``--list-test-cases`` argument. This ensures that the full
|
||||
list of tests is obtained. Since test discovery occurs at build time, it is
|
||||
not necessary to re-run CMake when the list of tests changes.
|
||||
However, it requires that :prop_tgt:`CROSSCOMPILING_EMULATOR` is properly set
|
||||
in order to function in a cross-compiling environment.
|
||||
|
||||
Additionally, setting properties on tests is somewhat less convenient, since
|
||||
the tests are not available at CMake time. Additional test properties may be
|
||||
assigned to the set of tests as a whole using the ``PROPERTIES`` option. If
|
||||
more fine-grained test control is needed, custom content may be provided
|
||||
through an external CTest script using the :prop_dir:`TEST_INCLUDE_FILES`
|
||||
directory property. The set of discovered tests is made accessible to such a
|
||||
script via the ``<target>_TESTS`` variable.
|
||||
|
||||
The options are:
|
||||
|
||||
``target``
|
||||
Specifies the doctest executable, which must be a known CMake executable
|
||||
target. CMake will substitute the location of the built executable when
|
||||
running the test.
|
||||
|
||||
``TEST_SPEC arg1...``
|
||||
Specifies test cases, wildcarded test cases, tags and tag expressions to
|
||||
pass to the doctest executable with the ``--list-test-cases`` argument.
|
||||
|
||||
``EXTRA_ARGS arg1...``
|
||||
Any extra arguments to pass on the command line to each test case.
|
||||
|
||||
``WORKING_DIRECTORY dir``
|
||||
Specifies the directory in which to run the discovered test cases. If this
|
||||
option is not provided, the current binary directory is used.
|
||||
|
||||
``TEST_PREFIX prefix``
|
||||
Specifies a ``prefix`` to be prepended to the name of each discovered test
|
||||
case. This can be useful when the same test executable is being used in
|
||||
multiple calls to ``doctest_discover_tests()`` but with different
|
||||
``TEST_SPEC`` or ``EXTRA_ARGS``.
|
||||
|
||||
``TEST_SUFFIX suffix``
|
||||
Similar to ``TEST_PREFIX`` except the ``suffix`` is appended to the name of
|
||||
every discovered test case. Both ``TEST_PREFIX`` and ``TEST_SUFFIX`` may
|
||||
be specified.
|
||||
|
||||
``PROPERTIES name1 value1...``
|
||||
Specifies additional properties to be set on all tests discovered by this
|
||||
invocation of ``doctest_discover_tests``.
|
||||
|
||||
``TEST_LIST var``
|
||||
Make the list of tests available in the variable ``var``, rather than the
|
||||
default ``<target>_TESTS``. This can be useful when the same test
|
||||
executable is being used in multiple calls to ``doctest_discover_tests()``.
|
||||
Note that this variable is only available in CTest.
|
||||
|
||||
#]=======================================================================]
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
function(doctest_discover_tests TARGET)
|
||||
cmake_parse_arguments(
|
||||
""
|
||||
""
|
||||
"TEST_PREFIX;TEST_SUFFIX;WORKING_DIRECTORY;TEST_LIST"
|
||||
"TEST_SPEC;EXTRA_ARGS;PROPERTIES"
|
||||
${ARGN}
|
||||
)
|
||||
|
||||
if(NOT _WORKING_DIRECTORY)
|
||||
set(_WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}")
|
||||
endif()
|
||||
if(NOT _TEST_LIST)
|
||||
set(_TEST_LIST ${TARGET}_TESTS)
|
||||
endif()
|
||||
|
||||
## Generate a unique name based on the extra arguments
|
||||
string(SHA1 args_hash "${_TEST_SPEC} ${_EXTRA_ARGS}")
|
||||
string(SUBSTRING ${args_hash} 0 7 args_hash)
|
||||
|
||||
# Define rule to generate test list for aforementioned test executable
|
||||
set(ctest_include_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_include-${args_hash}.cmake")
|
||||
set(ctest_tests_file "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}_tests-${args_hash}.cmake")
|
||||
get_property(crosscompiling_emulator
|
||||
TARGET ${TARGET}
|
||||
PROPERTY CROSSCOMPILING_EMULATOR
|
||||
)
|
||||
add_custom_command(
|
||||
TARGET ${TARGET} POST_BUILD
|
||||
BYPRODUCTS "${ctest_tests_file}"
|
||||
COMMAND "${CMAKE_COMMAND}"
|
||||
-D "TEST_TARGET=${TARGET}"
|
||||
-D "TEST_EXECUTABLE=$<TARGET_FILE:${TARGET}>"
|
||||
-D "TEST_EXECUTOR=${crosscompiling_emulator}"
|
||||
-D "TEST_WORKING_DIR=${_WORKING_DIRECTORY}"
|
||||
-D "TEST_SPEC=${_TEST_SPEC}"
|
||||
-D "TEST_EXTRA_ARGS=${_EXTRA_ARGS}"
|
||||
-D "TEST_PROPERTIES=${_PROPERTIES}"
|
||||
-D "TEST_PREFIX=${_TEST_PREFIX}"
|
||||
-D "TEST_SUFFIX=${_TEST_SUFFIX}"
|
||||
-D "TEST_LIST=${_TEST_LIST}"
|
||||
-D "CTEST_FILE=${ctest_tests_file}"
|
||||
-P "${_DOCTEST_DISCOVER_TESTS_SCRIPT}"
|
||||
VERBATIM
|
||||
)
|
||||
|
||||
file(WRITE "${ctest_include_file}"
|
||||
"if(EXISTS \"${ctest_tests_file}\")\n"
|
||||
" include(\"${ctest_tests_file}\")\n"
|
||||
"else()\n"
|
||||
" add_test(${TARGET}_NOT_BUILT-${args_hash} ${TARGET}_NOT_BUILT-${args_hash})\n"
|
||||
"endif()\n"
|
||||
)
|
||||
|
||||
if(NOT CMAKE_VERSION VERSION_LESS 3.10)
|
||||
# Add discovered tests to directory TEST_INCLUDE_FILES
|
||||
set_property(DIRECTORY
|
||||
APPEND PROPERTY TEST_INCLUDE_FILES "${ctest_include_file}"
|
||||
)
|
||||
else()
|
||||
# Add discovered tests as directory TEST_INCLUDE_FILE if possible
|
||||
get_property(test_include_file_set DIRECTORY PROPERTY TEST_INCLUDE_FILE SET)
|
||||
if(NOT ${test_include_file_set})
|
||||
set_property(DIRECTORY
|
||||
PROPERTY TEST_INCLUDE_FILE "${ctest_include_file}"
|
||||
)
|
||||
else()
|
||||
message(FATAL_ERROR
|
||||
"Cannot set more than one TEST_INCLUDE_FILE"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
endfunction()
|
||||
|
||||
###############################################################################
|
||||
|
||||
set(_DOCTEST_DISCOVER_TESTS_SCRIPT
|
||||
${CMAKE_CURRENT_LIST_DIR}/doctestAddTests.cmake
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
||||
# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
||||
# file Copyright.txt or https://cmake.org/licensing for details.
|
||||
|
||||
set(prefix "${TEST_PREFIX}")
|
||||
set(suffix "${TEST_SUFFIX}")
|
||||
set(spec ${TEST_SPEC})
|
||||
set(extra_args ${TEST_EXTRA_ARGS})
|
||||
set(properties ${TEST_PROPERTIES})
|
||||
set(script)
|
||||
set(suite)
|
||||
set(tests)
|
||||
|
||||
function(add_command NAME)
|
||||
set(_args "")
|
||||
foreach(_arg ${ARGN})
|
||||
if(_arg MATCHES "[^-./:a-zA-Z0-9_]")
|
||||
set(_args "${_args} [==[${_arg}]==]") # form a bracket_argument
|
||||
else()
|
||||
set(_args "${_args} ${_arg}")
|
||||
endif()
|
||||
endforeach()
|
||||
set(script "${script}${NAME}(${_args})\n" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Run test executable to get list of available tests
|
||||
if(NOT EXISTS "${TEST_EXECUTABLE}")
|
||||
message(FATAL_ERROR
|
||||
"Specified test executable '${TEST_EXECUTABLE}' does not exist"
|
||||
)
|
||||
endif()
|
||||
|
||||
if("${spec}" MATCHES .)
|
||||
set(spec "--test-case=${spec}")
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND ${TEST_EXECUTOR} "${TEST_EXECUTABLE}" ${spec} --list-test-cases
|
||||
OUTPUT_VARIABLE output
|
||||
RESULT_VARIABLE result
|
||||
)
|
||||
if(NOT ${result} EQUAL 0)
|
||||
message(FATAL_ERROR
|
||||
"Error running test executable '${TEST_EXECUTABLE}':\n"
|
||||
" Result: ${result}\n"
|
||||
" Output: ${output}\n"
|
||||
)
|
||||
endif()
|
||||
|
||||
string(REPLACE "\n" ";" output "${output}")
|
||||
|
||||
# Parse output
|
||||
foreach(line ${output})
|
||||
if("${line}" STREQUAL "===============================================================================" OR "${line}" MATCHES [==[^\[doctest\] ]==])
|
||||
continue()
|
||||
endif()
|
||||
set(test ${line})
|
||||
# use escape commas to handle properly test cases with commas inside the name
|
||||
string(REPLACE "," "\\," test_name ${test})
|
||||
# ...and add to script
|
||||
add_command(add_test
|
||||
"${prefix}${test}${suffix}"
|
||||
${TEST_EXECUTOR}
|
||||
"${TEST_EXECUTABLE}"
|
||||
"--test-case=${test_name}"
|
||||
${extra_args}
|
||||
)
|
||||
add_command(set_tests_properties
|
||||
"${prefix}${test}${suffix}"
|
||||
PROPERTIES
|
||||
WORKING_DIRECTORY "${TEST_WORKING_DIR}"
|
||||
${properties}
|
||||
)
|
||||
list(APPEND tests "${prefix}${test}${suffix}")
|
||||
endforeach()
|
||||
|
||||
# Create a list of all discovered tests, which users may use to e.g. set
|
||||
# properties on the tests
|
||||
add_command(set ${TEST_LIST} ${tests})
|
||||
|
||||
# Write CTest script
|
||||
file(WRITE "${CTEST_FILE}" "${script}")
|
||||
@@ -0,0 +1,215 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/io_service.hpp>
|
||||
#include <cppcoro/read_only_file.hpp>
|
||||
#include <cppcoro/write_only_file.hpp>
|
||||
#include <cppcoro/read_write_file.hpp>
|
||||
#include <cppcoro/task.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
#include <cppcoro/when_all.hpp>
|
||||
#include <cppcoro/cancellation_source.hpp>
|
||||
#include <cppcoro/on_scope_exit.hpp>
|
||||
|
||||
#include <random>
|
||||
#include <thread>
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
|
||||
#include "io_service_fixture.hpp"
|
||||
|
||||
#include <ostream>
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("file");
|
||||
|
||||
namespace fs = cppcoro::filesystem;
|
||||
|
||||
namespace
|
||||
{
|
||||
class temp_dir_fixture
|
||||
{
|
||||
public:
|
||||
|
||||
temp_dir_fixture()
|
||||
{
|
||||
auto tempDir = fs::temp_directory_path();
|
||||
|
||||
std::random_device random;
|
||||
for (int attempt = 1;; ++attempt)
|
||||
{
|
||||
m_path = tempDir / std::to_string(random());
|
||||
try
|
||||
{
|
||||
fs::create_directories(m_path);
|
||||
return;
|
||||
}
|
||||
catch (const fs::filesystem_error&)
|
||||
{
|
||||
if (attempt == 10)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
~temp_dir_fixture()
|
||||
{
|
||||
fs::remove_all(m_path);
|
||||
}
|
||||
|
||||
const cppcoro::filesystem::path& temp_dir()
|
||||
{
|
||||
return m_path;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
cppcoro::filesystem::path m_path;
|
||||
|
||||
};
|
||||
|
||||
class temp_dir_with_io_service_fixture :
|
||||
public io_service_fixture,
|
||||
public temp_dir_fixture
|
||||
{
|
||||
};
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(temp_dir_fixture, "write a file")
|
||||
{
|
||||
auto filePath = temp_dir() / "foo";
|
||||
|
||||
cppcoro::io_service ioService;
|
||||
|
||||
auto write = [&](cppcoro::io_service& ioService) -> cppcoro::task<>
|
||||
{
|
||||
std::printf(" starting write\n"); std::fflush(stdout);
|
||||
|
||||
auto f = cppcoro::write_only_file::open(ioService, filePath);
|
||||
|
||||
CHECK(f.size() == 0);
|
||||
|
||||
char buffer[1024];
|
||||
char c = 'a';
|
||||
for (int i = 0; i < sizeof(buffer); ++i, c = (c == 'z' ? 'a' : c + 1))
|
||||
{
|
||||
buffer[i] = c;
|
||||
}
|
||||
|
||||
for (int chunk = 0; chunk < 10; ++chunk)
|
||||
{
|
||||
co_await f.write(chunk * sizeof(buffer), buffer, sizeof(buffer));
|
||||
}
|
||||
};
|
||||
|
||||
auto read = [&](cppcoro::io_service& io) -> cppcoro::task<>
|
||||
{
|
||||
std::printf(" starting read\n"); std::fflush(stdout);
|
||||
|
||||
auto f = cppcoro::read_only_file::open(io, filePath);
|
||||
|
||||
const auto fileSize = f.size();
|
||||
|
||||
CHECK(fileSize == 10240);
|
||||
|
||||
char buffer[20];
|
||||
|
||||
for (std::uint64_t i = 0; i < fileSize;)
|
||||
{
|
||||
auto bytesRead = co_await f.read(i, buffer, 20);
|
||||
for (size_t j = 0; j < bytesRead; ++j, ++i)
|
||||
{
|
||||
CHECK(buffer[j] == ('a' + ((i % 1024) % 26)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all(
|
||||
[&]() -> cppcoro::task<int>
|
||||
{
|
||||
auto stopOnExit = cppcoro::on_scope_exit([&] { ioService.stop(); });
|
||||
co_await write(ioService);
|
||||
co_await read(ioService);
|
||||
co_return 0;
|
||||
}(),
|
||||
[&]() -> cppcoro::task<int>
|
||||
{
|
||||
ioService.process_events();
|
||||
co_return 0;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(temp_dir_with_io_service_fixture, "read write file")
|
||||
{
|
||||
auto run = [&]() -> cppcoro::task<>
|
||||
{
|
||||
cppcoro::io_work_scope ioScope{ io_service() };
|
||||
auto f = cppcoro::read_write_file::open(io_service(), temp_dir() / "foo.txt");
|
||||
|
||||
char buffer1[100];
|
||||
std::memset(buffer1, 0xAB, sizeof(buffer1));
|
||||
|
||||
co_await f.write(0, buffer1, sizeof(buffer1));
|
||||
|
||||
char buffer2[50];
|
||||
std::memset(buffer2, 0xCC, sizeof(buffer2));
|
||||
|
||||
co_await f.read(0, buffer2, 50);
|
||||
CHECK(std::memcmp(buffer1, buffer2, 50) == 0);
|
||||
|
||||
co_await f.read(50, buffer2, 50);
|
||||
CHECK(std::memcmp(buffer1 + 50, buffer2, 50) == 0);
|
||||
};
|
||||
|
||||
cppcoro::sync_wait(run());
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(temp_dir_with_io_service_fixture, "cancel read")
|
||||
{
|
||||
cppcoro::sync_wait([&]() -> cppcoro::task<>
|
||||
{
|
||||
cppcoro::io_work_scope ioScope{ io_service() };
|
||||
auto f = cppcoro::read_write_file::open(io_service(), temp_dir() / "foo.txt");
|
||||
|
||||
f.set_size(20 * 1024 * 1024);
|
||||
|
||||
cppcoro::cancellation_source canceller;
|
||||
|
||||
try
|
||||
{
|
||||
(void)co_await cppcoro::when_all(
|
||||
[&]() -> cppcoro::task<int>
|
||||
{
|
||||
const auto fileSize = f.size();
|
||||
const std::size_t bufferSize = 64 * 1024;
|
||||
auto buffer = std::make_unique<std::uint8_t[]>(bufferSize);
|
||||
std::uint64_t offset = 0;
|
||||
while (offset < fileSize)
|
||||
{
|
||||
auto bytesRead = co_await f.read(offset, buffer.get(), bufferSize, canceller.token());
|
||||
offset += bytesRead;
|
||||
}
|
||||
WARN("should have been cancelled");
|
||||
co_return 0;
|
||||
}(),
|
||||
[&]() -> cppcoro::task<int>
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
co_await io_service().schedule_after(1ms);
|
||||
canceller.request_cancellation();
|
||||
co_return 0;
|
||||
}());
|
||||
WARN("Expected exception to be thrown");
|
||||
}
|
||||
catch (const cppcoro::operation_cancelled&)
|
||||
{
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,412 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/generator.hpp>
|
||||
#include <cppcoro/on_scope_exit.hpp>
|
||||
#include <cppcoro/fmap.hpp>
|
||||
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <forward_list>
|
||||
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("generator");
|
||||
|
||||
using cppcoro::generator;
|
||||
|
||||
TEST_CASE("default-constructed generator is empty sequence")
|
||||
{
|
||||
generator<int> ints;
|
||||
CHECK(ints.begin() == ints.end());
|
||||
}
|
||||
|
||||
TEST_CASE("generator of arithmetic type returns by copy")
|
||||
{
|
||||
auto f = []() -> generator<float>
|
||||
{
|
||||
co_yield 1.0f;
|
||||
co_yield 2.0f;
|
||||
};
|
||||
|
||||
auto gen = f();
|
||||
auto iter = gen.begin();
|
||||
// TODO: Should this really be required?
|
||||
//static_assert(std::is_same<decltype(*iter), float>::value, "operator* should return float by value");
|
||||
CHECK(*iter == 1.0f);
|
||||
++iter;
|
||||
CHECK(*iter == 2.0f);
|
||||
++iter;
|
||||
CHECK(iter == gen.end());
|
||||
}
|
||||
|
||||
TEST_CASE("generator of reference returns by reference")
|
||||
{
|
||||
auto f = [](float& value) -> generator<float&>
|
||||
{
|
||||
co_yield value;
|
||||
};
|
||||
|
||||
float value = 1.0f;
|
||||
for (auto& x : f(value))
|
||||
{
|
||||
CHECK(&x == &value);
|
||||
x += 1.0f;
|
||||
}
|
||||
|
||||
CHECK(value == 2.0f);
|
||||
}
|
||||
|
||||
TEST_CASE("generator of const type")
|
||||
{
|
||||
auto fib = []() -> generator<const std::uint64_t>
|
||||
{
|
||||
std::uint64_t a = 0, b = 1;
|
||||
while (true)
|
||||
{
|
||||
co_yield b;
|
||||
b += std::exchange(a, b);
|
||||
}
|
||||
};
|
||||
|
||||
std::uint64_t count = 0;
|
||||
for (auto i : fib())
|
||||
{
|
||||
if (i > 1'000'000) {
|
||||
break;
|
||||
}
|
||||
++count;
|
||||
}
|
||||
|
||||
// 30th fib number is 832'040
|
||||
CHECK(count == 30);
|
||||
}
|
||||
|
||||
TEST_CASE("value-category of fmap() matches reference type")
|
||||
{
|
||||
using cppcoro::fmap;
|
||||
|
||||
auto checkIsRvalue = [](auto&& x) {
|
||||
static_assert(std::is_rvalue_reference_v<decltype(x)>);
|
||||
static_assert(!std::is_const_v<std::remove_reference_t<decltype(x)>>);
|
||||
CHECK(x == 123);
|
||||
return x;
|
||||
};
|
||||
auto checkIsLvalue = [](auto&& x) {
|
||||
static_assert(std::is_lvalue_reference_v<decltype(x)>);
|
||||
static_assert(!std::is_const_v<std::remove_reference_t<decltype(x)>>);
|
||||
CHECK(x == 123);
|
||||
return x;
|
||||
};
|
||||
auto checkIsConstLvalue = [](auto&& x) {
|
||||
static_assert(std::is_lvalue_reference_v<decltype(x)>);
|
||||
static_assert(std::is_const_v<std::remove_reference_t<decltype(x)>>);
|
||||
CHECK(x == 123);
|
||||
return x;
|
||||
};
|
||||
auto checkIsConstRvalue = [](auto&& x) {
|
||||
static_assert(std::is_rvalue_reference_v<decltype(x)>);
|
||||
static_assert(std::is_const_v<std::remove_reference_t<decltype(x)>>);
|
||||
CHECK(x == 123);
|
||||
return x;
|
||||
};
|
||||
|
||||
auto consume = [](auto&& range) {
|
||||
for (auto&& x : range) {
|
||||
(void)x;
|
||||
}
|
||||
};
|
||||
|
||||
consume([]() -> generator<int> { co_yield 123; }() | fmap(checkIsLvalue));
|
||||
consume([]() -> generator<const int> { co_yield 123; }() | fmap(checkIsConstLvalue));
|
||||
consume([]() -> generator<int&> { co_yield 123; }() | fmap(checkIsLvalue));
|
||||
consume([]() -> generator<const int&> { co_yield 123; }() | fmap(checkIsConstLvalue));
|
||||
consume([]() -> generator<int&&> { co_yield 123; }() | fmap(checkIsRvalue));
|
||||
consume([]() -> generator<const int&&> { co_yield 123; }() | fmap(checkIsConstRvalue));
|
||||
}
|
||||
|
||||
TEST_CASE("generator doesn't start until its called")
|
||||
{
|
||||
bool reachedA = false;
|
||||
bool reachedB = false;
|
||||
bool reachedC = false;
|
||||
auto f = [&]() -> generator<int>
|
||||
{
|
||||
reachedA = true;
|
||||
co_yield 1;
|
||||
reachedB = true;
|
||||
co_yield 2;
|
||||
reachedC = true;
|
||||
};
|
||||
|
||||
auto gen = f();
|
||||
CHECK(!reachedA);
|
||||
auto iter = gen.begin();
|
||||
CHECK(reachedA);
|
||||
CHECK(!reachedB);
|
||||
CHECK(*iter == 1);
|
||||
++iter;
|
||||
CHECK(reachedB);
|
||||
CHECK(!reachedC);
|
||||
CHECK(*iter == 2);
|
||||
++iter;
|
||||
CHECK(reachedC);
|
||||
CHECK(iter == gen.end());
|
||||
}
|
||||
|
||||
TEST_CASE("destroying generator before completion destructs objects on stack")
|
||||
{
|
||||
bool destructed = false;
|
||||
bool completed = false;
|
||||
auto f = [&]() -> generator<int>
|
||||
{
|
||||
auto onExit = cppcoro::on_scope_exit([&]
|
||||
{
|
||||
destructed = true;
|
||||
});
|
||||
|
||||
co_yield 1;
|
||||
co_yield 2;
|
||||
completed = true;
|
||||
};
|
||||
|
||||
{
|
||||
auto g = f();
|
||||
auto it = g.begin();
|
||||
auto itEnd = g.end();
|
||||
CHECK(it != itEnd);
|
||||
CHECK(*it == 1u);
|
||||
CHECK(!destructed);
|
||||
}
|
||||
|
||||
CHECK(!completed);
|
||||
CHECK(destructed);
|
||||
}
|
||||
|
||||
TEST_CASE("generator throwing before yielding first element rethrows out of begin()")
|
||||
{
|
||||
class X {};
|
||||
|
||||
auto g = []() -> cppcoro::generator<int>
|
||||
{
|
||||
throw X{};
|
||||
co_return;
|
||||
}();
|
||||
|
||||
try
|
||||
{
|
||||
g.begin();
|
||||
FAIL("should have thrown");
|
||||
}
|
||||
catch (const X&)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("generator throwing after first element rethrows out of operator++")
|
||||
{
|
||||
class X {};
|
||||
|
||||
auto g = []() -> cppcoro::generator<int>
|
||||
{
|
||||
co_yield 1;
|
||||
throw X{};
|
||||
}();
|
||||
|
||||
auto iter = g.begin();
|
||||
REQUIRE(iter != g.end());
|
||||
try
|
||||
{
|
||||
++iter;
|
||||
FAIL("should have thrown");
|
||||
}
|
||||
catch (const X&)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
template<typename FIRST, typename SECOND>
|
||||
auto concat(FIRST&& first, SECOND&& second)
|
||||
{
|
||||
using value_type = std::remove_reference_t<decltype(*first.begin())>;
|
||||
return [](FIRST first, SECOND second) -> cppcoro::generator<value_type>
|
||||
{
|
||||
for (auto&& x : first) co_yield x;
|
||||
for (auto&& y : second) co_yield y;
|
||||
}(std::forward<FIRST>(first), std::forward<SECOND>(second));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("safe capture of r-value reference args")
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
|
||||
// Check that we can capture l-values by reference and that temporary
|
||||
// values are moved into the coroutine frame.
|
||||
std::string byRef = "bar";
|
||||
auto g = concat("foo"s, concat(byRef, std::vector<char>{ 'b', 'a', 'z' }));
|
||||
|
||||
byRef = "buzz";
|
||||
|
||||
std::string s;
|
||||
for (char c : g)
|
||||
{
|
||||
s += c;
|
||||
}
|
||||
|
||||
CHECK(s == "foobuzzbaz");
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
cppcoro::generator<int> range(int start, int end)
|
||||
{
|
||||
for (; start < end; ++start)
|
||||
{
|
||||
co_yield start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("fmap operator")
|
||||
{
|
||||
cppcoro::generator<int> gen = range(0, 5)
|
||||
| cppcoro::fmap([](int x) { return x * 3; });
|
||||
|
||||
auto it = gen.begin();
|
||||
CHECK(*it == 0);
|
||||
CHECK(*++it == 3);
|
||||
CHECK(*++it == 6);
|
||||
CHECK(*++it == 9);
|
||||
CHECK(*++it == 12);
|
||||
CHECK(++it == gen.end());
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
template<std::size_t window, typename Range>
|
||||
cppcoro::generator<const double> low_pass(Range rng)
|
||||
{
|
||||
auto it = std::begin(rng);
|
||||
const auto itEnd = std::end(rng);
|
||||
|
||||
const double invCount = 1.0 / window;
|
||||
double sum = 0;
|
||||
|
||||
using iter_cat =
|
||||
typename std::iterator_traits<decltype(it)>::iterator_category;
|
||||
|
||||
if constexpr (std::is_base_of_v<std::random_access_iterator_tag, iter_cat>)
|
||||
{
|
||||
for (std::size_t count = 0; it != itEnd && count < window; ++it)
|
||||
{
|
||||
sum += *it;
|
||||
++count;
|
||||
co_yield sum / count;
|
||||
}
|
||||
|
||||
for (; it != itEnd; ++it)
|
||||
{
|
||||
sum -= *(it - window);
|
||||
sum += *it;
|
||||
co_yield sum * invCount;
|
||||
}
|
||||
}
|
||||
else if constexpr (std::is_base_of_v<std::forward_iterator_tag, iter_cat>)
|
||||
{
|
||||
auto windowStart = it;
|
||||
for (std::size_t count = 0; it != itEnd && count < window; ++it)
|
||||
{
|
||||
sum += *it;
|
||||
++count;
|
||||
co_yield sum / count;
|
||||
}
|
||||
|
||||
for (; it != itEnd; ++it, ++windowStart)
|
||||
{
|
||||
sum -= *windowStart;
|
||||
sum += *it;
|
||||
co_yield sum * invCount;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Just assume an input iterator
|
||||
double buffer[window];
|
||||
|
||||
for (std::size_t count = 0; it != itEnd && count < window; ++it)
|
||||
{
|
||||
buffer[count] = *it;
|
||||
sum += buffer[count];
|
||||
++count;
|
||||
co_yield sum / count;
|
||||
}
|
||||
|
||||
for (std::size_t pos = 0; it != itEnd; ++it, pos = (pos + 1 == window) ? 0 : (pos + 1))
|
||||
{
|
||||
sum -= std::exchange(buffer[pos], *it);
|
||||
sum += buffer[pos];
|
||||
co_yield sum * invCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HACK: Disable this test as it's causing heap corruption errors under MSVC 2017 Update 5 x86 debug builds.
|
||||
// Still needs investigation of root cause.
|
||||
TEST_CASE("low_pass" * doctest::skip{ true })
|
||||
{
|
||||
// With random-access iterator
|
||||
{
|
||||
auto gen = low_pass<4>(std::vector<int>{ 10, 13, 10, 15, 18, 9, 11, 15 });
|
||||
auto it = gen.begin();
|
||||
CHECK(*it == 10.0);
|
||||
CHECK(*++it == 11.5);
|
||||
CHECK(*++it == 11.0);
|
||||
CHECK(*++it == 12.0);
|
||||
CHECK(*++it == 14.0);
|
||||
CHECK(*++it == 13.0);
|
||||
CHECK(*++it == 13.25);
|
||||
CHECK(*++it == 13.25);
|
||||
CHECK(++it == gen.end());
|
||||
}
|
||||
|
||||
// With forward-iterator
|
||||
{
|
||||
auto gen = low_pass<4>(std::forward_list<int>{ 10, 13, 10, 15, 18, 9, 11, 15 });
|
||||
auto it = gen.begin();
|
||||
CHECK(*it == 10.0);
|
||||
CHECK(*++it == 11.5);
|
||||
CHECK(*++it == 11.0);
|
||||
CHECK(*++it == 12.0);
|
||||
CHECK(*++it == 14.0);
|
||||
CHECK(*++it == 13.0);
|
||||
CHECK(*++it == 13.25);
|
||||
CHECK(*++it == 13.25);
|
||||
CHECK(++it == gen.end());
|
||||
}
|
||||
|
||||
// With input-iterator
|
||||
{
|
||||
auto gen = low_pass<3>(range(10, 20));
|
||||
auto it = gen.begin();
|
||||
CHECK(*it == 10.0);
|
||||
CHECK(*++it == 10.5);
|
||||
CHECK(*++it == 11.0);
|
||||
CHECK(*++it == 12.0);
|
||||
CHECK(*++it == 13.0);
|
||||
CHECK(*++it == 14.0);
|
||||
CHECK(*++it == 15.0);
|
||||
CHECK(*++it == 16.0);
|
||||
CHECK(*++it == 17.0);
|
||||
CHECK(*++it == 18.0);
|
||||
CHECK(++it == gen.end());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,71 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
#ifndef CPPCORO_TESTS_IO_SERVICE_FIXTURE_HPP_INCLUDED
|
||||
#define CPPCORO_TESTS_IO_SERVICE_FIXTURE_HPP_INCLUDED
|
||||
|
||||
#include <cppcoro/io_service.hpp>
|
||||
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
/// \brief
|
||||
/// Test fixture that creates an io_service and starts up a background thread
|
||||
/// to process I/O completion events.
|
||||
///
|
||||
/// Thread and io_service are shutdown on destruction.
|
||||
struct io_service_fixture
|
||||
{
|
||||
public:
|
||||
|
||||
io_service_fixture(std::uint32_t threadCount = 1)
|
||||
: m_ioService()
|
||||
{
|
||||
m_ioThreads.reserve(threadCount);
|
||||
try
|
||||
{
|
||||
for (std::uint32_t i = 0; i < threadCount; ++i)
|
||||
{
|
||||
m_ioThreads.emplace_back([this] { m_ioService.process_events(); });
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
stop();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
~io_service_fixture()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
cppcoro::io_service& io_service() { return m_ioService; }
|
||||
|
||||
private:
|
||||
|
||||
void stop()
|
||||
{
|
||||
m_ioService.stop();
|
||||
for (auto& thread : m_ioThreads)
|
||||
{
|
||||
thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
cppcoro::io_service m_ioService;
|
||||
std::vector<std::thread> m_ioThreads;
|
||||
|
||||
};
|
||||
|
||||
template<std::uint32_t thread_count>
|
||||
struct io_service_fixture_with_threads : io_service_fixture
|
||||
{
|
||||
io_service_fixture_with_threads()
|
||||
: io_service_fixture(thread_count)
|
||||
{}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,230 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/io_service.hpp>
|
||||
#include <cppcoro/task.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
#include <cppcoro/when_all.hpp>
|
||||
#include <cppcoro/when_all_ready.hpp>
|
||||
#include <cppcoro/on_scope_exit.hpp>
|
||||
#include <cppcoro/operation_cancelled.hpp>
|
||||
#include <cppcoro/cancellation_source.hpp>
|
||||
|
||||
#include "io_service_fixture.hpp"
|
||||
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <ostream>
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("io_service");
|
||||
|
||||
TEST_CASE("default construct")
|
||||
{
|
||||
cppcoro::io_service service;
|
||||
CHECK_FALSE(service.is_stop_requested());
|
||||
}
|
||||
|
||||
TEST_CASE("construct with concurrency hint")
|
||||
{
|
||||
cppcoro::io_service service{ 3 };
|
||||
CHECK_FALSE(service.is_stop_requested());
|
||||
}
|
||||
|
||||
TEST_CASE("process_one_pending_event returns immediately when no events")
|
||||
{
|
||||
cppcoro::io_service service;
|
||||
CHECK(service.process_one_pending_event() == 0);
|
||||
CHECK(service.process_pending_events() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("schedule coroutine")
|
||||
{
|
||||
cppcoro::io_service service;
|
||||
|
||||
bool reachedPointA = false;
|
||||
bool reachedPointB = false;
|
||||
auto startTask = [&](cppcoro::io_service& ioService) -> cppcoro::task<>
|
||||
{
|
||||
reachedPointA = true;
|
||||
co_await ioService.schedule();
|
||||
reachedPointB = true;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
startTask(service),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(reachedPointA);
|
||||
CHECK_FALSE(reachedPointB);
|
||||
|
||||
service.process_pending_events();
|
||||
|
||||
CHECK(reachedPointB);
|
||||
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(io_service_fixture_with_threads<2>, "multiple I/O threads servicing events")
|
||||
{
|
||||
std::atomic<int> completedCount = 0;
|
||||
|
||||
auto runOnIoThread = [&]() -> cppcoro::task<>
|
||||
{
|
||||
co_await io_service().schedule();
|
||||
++completedCount;
|
||||
};
|
||||
|
||||
std::vector<cppcoro::task<>> tasks;
|
||||
{
|
||||
for (int i = 0; i < 1000; ++i)
|
||||
{
|
||||
tasks.emplace_back(runOnIoThread());
|
||||
}
|
||||
}
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all(std::move(tasks)));
|
||||
|
||||
CHECK(completedCount == 1000);
|
||||
}
|
||||
|
||||
TEST_CASE("Multiple concurrent timers")
|
||||
{
|
||||
cppcoro::io_service ioService;
|
||||
|
||||
auto startTimer = [&](std::chrono::milliseconds duration)
|
||||
-> cppcoro::task<std::chrono::high_resolution_clock::duration>
|
||||
{
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
co_await ioService.schedule_after(duration);
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
|
||||
co_return end - start;
|
||||
};
|
||||
|
||||
auto test = [&]() -> cppcoro::task<>
|
||||
{
|
||||
using namespace std::chrono;
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
auto[time1, time2, time3] = co_await cppcoro::when_all(
|
||||
startTimer(100ms),
|
||||
startTimer(120ms),
|
||||
startTimer(50ms));
|
||||
|
||||
MESSAGE("Waiting 100ms took " << duration_cast<microseconds>(time1).count() << "us");
|
||||
MESSAGE("Waiting 120ms took " << duration_cast<microseconds>(time2).count() << "us");
|
||||
MESSAGE("Waiting 50ms took " << duration_cast<microseconds>(time3).count() << "us");
|
||||
|
||||
CHECK(time1 >= 100ms);
|
||||
CHECK(time2 >= 120ms);
|
||||
CHECK(time3 >= 50ms);
|
||||
};
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
auto stopIoOnExit = cppcoro::on_scope_exit([&] { ioService.stop(); });
|
||||
co_await test();
|
||||
}(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
ioService.process_events();
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_CASE("Timer cancellation"
|
||||
* doctest::timeout{ 5.0 })
|
||||
{
|
||||
using namespace std::literals::chrono_literals;
|
||||
|
||||
cppcoro::io_service ioService;
|
||||
|
||||
auto longWait = [&](cppcoro::cancellation_token ct) -> cppcoro::task<>
|
||||
{
|
||||
co_await ioService.schedule_after(20'000ms, ct);
|
||||
};
|
||||
|
||||
auto cancelAfter = [&](cppcoro::cancellation_source source, auto duration) -> cppcoro::task<>
|
||||
{
|
||||
co_await ioService.schedule_after(duration);
|
||||
source.request_cancellation();
|
||||
};
|
||||
|
||||
auto test = [&]() -> cppcoro::task<>
|
||||
{
|
||||
cppcoro::cancellation_source source;
|
||||
co_await cppcoro::when_all_ready(
|
||||
[&](cppcoro::cancellation_token ct) -> cppcoro::task<>
|
||||
{
|
||||
CHECK_THROWS_AS(co_await longWait(std::move(ct)), const cppcoro::operation_cancelled&);
|
||||
}(source.token()),
|
||||
cancelAfter(source, 1ms));
|
||||
};
|
||||
|
||||
auto testTwice = [&]() -> cppcoro::task<>
|
||||
{
|
||||
co_await test();
|
||||
co_await test();
|
||||
};
|
||||
|
||||
auto stopIoServiceAfter = [&](cppcoro::task<> task) -> cppcoro::task<>
|
||||
{
|
||||
co_await task.when_ready();
|
||||
ioService.stop();
|
||||
co_return co_await task.when_ready();
|
||||
};
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
stopIoServiceAfter(testTwice()),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
ioService.process_events();
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(io_service_fixture_with_threads<1>, "Many concurrent timers")
|
||||
{
|
||||
auto startTimer = [&]() -> cppcoro::task<>
|
||||
{
|
||||
using namespace std::literals::chrono_literals;
|
||||
co_await io_service().schedule_after(50ms);
|
||||
};
|
||||
|
||||
constexpr std::uint32_t taskCount = 10'000;
|
||||
|
||||
auto runManyTimers = [&]() -> cppcoro::task<>
|
||||
{
|
||||
std::vector<cppcoro::task<>> tasks;
|
||||
|
||||
tasks.reserve(taskCount);
|
||||
|
||||
for (std::uint32_t i = 0; i < taskCount; ++i)
|
||||
{
|
||||
tasks.emplace_back(startTimer());
|
||||
}
|
||||
|
||||
co_await cppcoro::when_all(std::move(tasks));
|
||||
};
|
||||
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
cppcoro::sync_wait(runManyTimers());
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
|
||||
MESSAGE(
|
||||
"Waiting for " << taskCount << " x 50ms timers took "
|
||||
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
|
||||
<< "ms");
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,45 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/net/ip_address.hpp>
|
||||
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("ip_address");
|
||||
|
||||
using cppcoro::net::ip_address;
|
||||
using cppcoro::net::ipv4_address;
|
||||
using cppcoro::net::ipv6_address;
|
||||
|
||||
TEST_CASE("default constructor")
|
||||
{
|
||||
ip_address x;
|
||||
CHECK(x.is_ipv4());
|
||||
CHECK(x.to_ipv4() == ipv4_address{});
|
||||
}
|
||||
|
||||
TEST_CASE("to_string")
|
||||
{
|
||||
ip_address a = ipv6_address{ 0xAABBCCDD00112233, 0x0102030405060708 };
|
||||
ip_address b = ipv4_address{ 192, 168, 0, 1 };
|
||||
|
||||
CHECK(a.to_string() == "aabb:ccdd:11:2233:102:304:506:708");
|
||||
CHECK(b.to_string() == "192.168.0.1");
|
||||
}
|
||||
|
||||
TEST_CASE("from_string")
|
||||
{
|
||||
CHECK(ip_address::from_string("") == std::nullopt);
|
||||
CHECK(ip_address::from_string("foo") == std::nullopt);
|
||||
CHECK(ip_address::from_string(" 192.168.0.1") == std::nullopt);
|
||||
CHECK(ip_address::from_string("192.168.0.1asdf") == std::nullopt);
|
||||
|
||||
CHECK(ip_address::from_string("192.168.0.1") == ipv4_address(192, 168, 0, 1));
|
||||
CHECK(ip_address::from_string("::192.168.0.1") == ipv6_address(0, 0, 0, 0, 0, 0, 0xc0a8, 0x1));
|
||||
CHECK(ip_address::from_string("aabb:ccdd:11:2233:102:304:506:708") ==
|
||||
ipv6_address{ 0xAABBCCDD00112233, 0x0102030405060708 });
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,56 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/config.hpp>
|
||||
#include <cppcoro/net/ip_endpoint.hpp>
|
||||
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("ip_endpoint");
|
||||
|
||||
using namespace cppcoro::net;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr bool isMsvc15_5X86Optimised =
|
||||
#if CPPCORO_COMPILER_MSVC && CPPCORO_CPU_X86 && _MSC_VER == 1912 && defined(CPPCORO_RELEASE_OPTIMISED)
|
||||
true;
|
||||
#else
|
||||
false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// BUG: Skip this test under MSVC 15.5 x86 optimised builds due to a compiler bug
|
||||
// that generates bad code.
|
||||
// See https://developercommunity.visualstudio.com/content/problem/177151/bad-code-generation-under-x86-optimised-for-stdopt.html
|
||||
TEST_CASE("to_string" * doctest::skip{ isMsvc15_5X86Optimised })
|
||||
{
|
||||
ip_endpoint a = ipv4_endpoint{ ipv4_address{ 192, 168, 2, 254 }, 80 };
|
||||
ip_endpoint b = ipv6_endpoint{
|
||||
*ipv6_address::from_string("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
|
||||
22 };
|
||||
|
||||
CHECK(a.to_string() == "192.168.2.254:80");
|
||||
CHECK(b.to_string() == "[2001:db8:85a3::8a2e:370:7334]:22");
|
||||
}
|
||||
|
||||
TEST_CASE("from_string" * doctest::skip{ isMsvc15_5X86Optimised })
|
||||
{
|
||||
CHECK(ip_endpoint::from_string("") == std::nullopt);
|
||||
CHECK(ip_endpoint::from_string("[foo]:123") == std::nullopt);
|
||||
CHECK(ip_endpoint::from_string("[123]:1000") == std::nullopt);
|
||||
CHECK(ip_endpoint::from_string("[10.11.12.13]:1000") == std::nullopt);
|
||||
|
||||
CHECK(ip_endpoint::from_string("192.168.2.254:80") ==
|
||||
ipv4_endpoint{
|
||||
ipv4_address{ 192, 168, 2, 254 }, 80 });
|
||||
CHECK(ip_endpoint::from_string("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:443") ==
|
||||
ipv6_endpoint{
|
||||
ipv6_address{ 0x2001, 0xdb8, 0x85a3, 0x0, 0x0, 0x8a2e, 0x370, 0x7334 },
|
||||
443 });
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/net/ipv4_address.hpp>
|
||||
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
|
||||
TEST_SUITE_BEGIN("ipv4_address");
|
||||
|
||||
using cppcoro::net::ipv4_address;
|
||||
|
||||
TEST_CASE("DefaultConstructToZeroes")
|
||||
{
|
||||
CHECK(ipv4_address{}.to_integer() == 0u);
|
||||
}
|
||||
|
||||
TEST_CASE("to_integer() is BigEndian")
|
||||
{
|
||||
ipv4_address address{ 10, 11, 12, 13 };
|
||||
CHECK(address.to_integer() == 0x0A0B0C0Du);
|
||||
}
|
||||
|
||||
TEST_CASE("is_loopback()")
|
||||
{
|
||||
CHECK(ipv4_address{ 127, 0, 0, 1 }.is_loopback());
|
||||
CHECK(ipv4_address{ 127, 0, 0, 50 }.is_loopback());
|
||||
CHECK(ipv4_address{ 127, 5, 10, 15 }.is_loopback());
|
||||
CHECK(!ipv4_address{ 10, 11, 12, 13 }.is_loopback());
|
||||
}
|
||||
|
||||
TEST_CASE("bytes()")
|
||||
{
|
||||
ipv4_address ip{ 19, 63, 129, 200 };
|
||||
CHECK(ip.bytes()[0] == 19);
|
||||
CHECK(ip.bytes()[1] == 63);
|
||||
CHECK(ip.bytes()[2] == 129);
|
||||
CHECK(ip.bytes()[3] == 200);
|
||||
}
|
||||
|
||||
TEST_CASE("to_string()")
|
||||
{
|
||||
CHECK(ipv4_address(0, 0, 0, 0).to_string() == "0.0.0.0");
|
||||
CHECK(ipv4_address(10, 125, 255, 7).to_string() == "10.125.255.7");
|
||||
CHECK(ipv4_address(123, 234, 101, 255).to_string() == "123.234.101.255");
|
||||
}
|
||||
|
||||
TEST_CASE("from_string")
|
||||
{
|
||||
// Check for some invalid strings.
|
||||
CHECK(ipv4_address::from_string("") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string("asdf") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string(" 123.34.56.8") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string("123.34.56.8 ") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string("123.") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string("123.1") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string("123.12") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string("123.12.") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string("123.12.4") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string("123.12.45") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string("123.12.45.") == std::nullopt);
|
||||
|
||||
// Overflow of individual parts
|
||||
CHECK(ipv4_address::from_string("456.12.45.30") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string("45.256.45.30") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string("45.25.677.30") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string("123.12.45.301") == std::nullopt);
|
||||
|
||||
// Can't parse octal yet.
|
||||
CHECK(ipv4_address::from_string("00") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string("012345") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string("045.25.67.30") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string("45.025.67.30") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string("45.25.067.30") == std::nullopt);
|
||||
CHECK(ipv4_address::from_string("45.25.67.030") == std::nullopt);
|
||||
|
||||
// Parse single integer format
|
||||
CHECK(ipv4_address::from_string("0") == ipv4_address(0));
|
||||
CHECK(ipv4_address::from_string("1") == ipv4_address(0, 0, 0, 1));
|
||||
CHECK(ipv4_address::from_string("255") == ipv4_address(0, 0, 0, 255));
|
||||
CHECK(ipv4_address::from_string("43534243") == ipv4_address(43534243));
|
||||
|
||||
// Parse dotted decimal format
|
||||
CHECK(ipv4_address::from_string("45.25.67.30") == ipv4_address(45, 25, 67, 30));
|
||||
CHECK(ipv4_address::from_string("0.0.0.0") == ipv4_address(0, 0, 0, 0));
|
||||
CHECK(ipv4_address::from_string("1.2.3.4") == ipv4_address(1, 2, 3, 4));
|
||||
}
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,33 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/net/ipv4_endpoint.hpp>
|
||||
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("ip_endpoint");
|
||||
|
||||
using namespace cppcoro::net;
|
||||
|
||||
TEST_CASE("to_string")
|
||||
{
|
||||
CHECK(ipv4_endpoint{ ipv4_address{ 192, 168, 2, 254 }, 80 }.to_string() == "192.168.2.254:80");
|
||||
}
|
||||
|
||||
TEST_CASE("from_string")
|
||||
{
|
||||
CHECK(ipv4_endpoint::from_string("") == std::nullopt);
|
||||
CHECK(ipv4_endpoint::from_string(" ") == std::nullopt);
|
||||
CHECK(ipv4_endpoint::from_string("100") == std::nullopt);
|
||||
CHECK(ipv4_endpoint::from_string("100.10.200.20") == std::nullopt);
|
||||
CHECK(ipv4_endpoint::from_string("100.10.200.20:") == std::nullopt);
|
||||
CHECK(ipv4_endpoint::from_string("100.10.200.20::80") == std::nullopt);
|
||||
CHECK(ipv4_endpoint::from_string("100.10.200.20 80") == std::nullopt);
|
||||
|
||||
CHECK(ipv4_endpoint::from_string("192.168.2.254:80") ==
|
||||
ipv4_endpoint{ ipv4_address{ 192, 168, 2, 254 }, 80 });
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,154 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/net/ipv6_address.hpp>
|
||||
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
|
||||
TEST_SUITE_BEGIN("ipv6_address");
|
||||
|
||||
using cppcoro::net::ipv6_address;
|
||||
|
||||
TEST_CASE("default constructor")
|
||||
{
|
||||
ipv6_address zero;
|
||||
for (std::uint8_t i = 0; i < 16; ++i)
|
||||
{
|
||||
CHECK(zero.bytes()[i] == 0);
|
||||
}
|
||||
|
||||
CHECK(zero == ipv6_address::unspecified());
|
||||
}
|
||||
|
||||
TEST_CASE("to_string")
|
||||
{
|
||||
CHECK(ipv6_address(0, 0).to_string() == "::");
|
||||
CHECK(ipv6_address::loopback().to_string() == "::1");
|
||||
|
||||
CHECK(
|
||||
ipv6_address(0x0102030405060708, 0x090A0B0C0D0E0F10).to_string() ==
|
||||
"102:304:506:708:90a:b0c:d0e:f10");
|
||||
CHECK(
|
||||
ipv6_address(0x0001001001001000, 0x0).to_string() ==
|
||||
"1:10:100:1000::");
|
||||
CHECK(
|
||||
ipv6_address(0x0002030405060708, 0x090A0B0C0D0E0F10).to_string() ==
|
||||
"2:304:506:708:90a:b0c:d0e:f10");
|
||||
CHECK(
|
||||
ipv6_address(0x0000030405060708, 0x090A0B0C0D0E0F10).to_string() ==
|
||||
"0:304:506:708:90a:b0c:d0e:f10");
|
||||
CHECK(
|
||||
ipv6_address(0x0000000005060708, 0x090A0B0C0D0E0F10).to_string() ==
|
||||
"::506:708:90a:b0c:d0e:f10");
|
||||
CHECK(
|
||||
ipv6_address(0x0102030400000000, 0x00000B0C0D0E0F10).to_string() ==
|
||||
"102:304::b0c:d0e:f10");
|
||||
CHECK(
|
||||
ipv6_address(0x0102030405060708, 0x090A0B0C0D0E0000).to_string() ==
|
||||
"102:304:506:708:90a:b0c:d0e:0");
|
||||
CHECK(
|
||||
ipv6_address(0x0102030405060708, 0x090A0B0C00000000).to_string() ==
|
||||
"102:304:506:708:90a:b0c::");
|
||||
|
||||
// Check that it contracts the first of multiple equal-length zero runs.
|
||||
CHECK(
|
||||
ipv6_address(0x0102030400000000, 0x090A0B0C00000000).to_string() ==
|
||||
"102:304::90a:b0c:0:0");
|
||||
}
|
||||
|
||||
TEST_CASE("from_string")
|
||||
{
|
||||
CHECK(ipv6_address::from_string("") == std::nullopt);
|
||||
CHECK(ipv6_address::from_string("123") == std::nullopt);
|
||||
CHECK(ipv6_address::from_string("foo") == std::nullopt);
|
||||
CHECK(ipv6_address::from_string(":1234") == std::nullopt);
|
||||
CHECK(ipv6_address::from_string("0102:0304:0506:0708:090a:0b0c:0d0e:0f10 ") == std::nullopt);
|
||||
CHECK(
|
||||
ipv6_address::from_string(" 0102:0304:0506:0708:090a:0b0c:0d0e:0f10") ==
|
||||
std::nullopt);
|
||||
CHECK(
|
||||
ipv6_address::from_string("0102:0304:0506:0708:090a:0b0c:0d0e:0f10:") ==
|
||||
std::nullopt);
|
||||
CHECK(
|
||||
ipv6_address::from_string("0102:0304:0506:0708:090a:0b0c:0d0e") ==
|
||||
std::nullopt);
|
||||
CHECK(
|
||||
ipv6_address::from_string("01022:0304:0506:0708:090a:0b0c:0d0e:0f10") ==
|
||||
std::nullopt);
|
||||
CHECK(
|
||||
ipv6_address::from_string("0102:0304:0506:192.168.0.1:0b0c:0d0e:0f10") ==
|
||||
std::nullopt);
|
||||
CHECK(ipv6_address::from_string("::") == ipv6_address(0, 0));
|
||||
CHECK(ipv6_address::from_string("::1") == ipv6_address::loopback());
|
||||
CHECK(ipv6_address::from_string("::01") == ipv6_address::loopback());
|
||||
CHECK(ipv6_address::from_string("::001") == ipv6_address::loopback());
|
||||
CHECK(ipv6_address::from_string("::0001") == ipv6_address::loopback());
|
||||
CHECK(
|
||||
ipv6_address::from_string("0102:0304:0506:0708:090a:0b0c:0d0e:0f10") ==
|
||||
ipv6_address(0x0102030405060708, 0x090A0B0C0D0E0F10));
|
||||
CHECK(
|
||||
ipv6_address::from_string("0002:0304:0506:0708:090a:0b0c:0d0e:0f10") ==
|
||||
ipv6_address(0x0002030405060708, 0x090A0B0C0D0E0F10));
|
||||
CHECK(
|
||||
ipv6_address::from_string("0000:0304:0506:0708:090a:0b0c:0d0e:0f10") ==
|
||||
ipv6_address(0x0000030405060708, 0x090A0B0C0D0E0F10));
|
||||
CHECK(
|
||||
ipv6_address::from_string("::0506:0708:090a:0b0c:0d0e:0f10") ==
|
||||
ipv6_address(0x0000000005060708, 0x090A0B0C0D0E0F10));
|
||||
CHECK(
|
||||
ipv6_address::from_string("0102:0304::0b0c:0d0e:0f10") ==
|
||||
ipv6_address(0x0102030400000000, 0x00000B0C0D0E0F10));
|
||||
CHECK(
|
||||
ipv6_address::from_string("0102:0304:0506:0708:090a:0b0c::") ==
|
||||
ipv6_address(0x0102030405060708, 0x090A0B0C00000000));
|
||||
CHECK(
|
||||
ipv6_address::from_string("2001:db8:85a3:8d3:1319:8a2e:370:7348") ==
|
||||
ipv6_address(0x20010db885a308d3, 0x13198a2e03707348));
|
||||
}
|
||||
|
||||
TEST_CASE("from_string IPv4 interop format")
|
||||
{
|
||||
CHECK(
|
||||
ipv6_address::from_string("::ffff:192.168.0.1") ==
|
||||
ipv6_address(0x0, 0xffffc0a80001));
|
||||
CHECK(
|
||||
ipv6_address::from_string("0102:0304::128.69.32.17") ==
|
||||
ipv6_address(0x0102030400000000, 0x0000000080452011));
|
||||
CHECK(
|
||||
ipv6_address::from_string("0102:0304::128.69.32.17") ==
|
||||
ipv6_address(0x0102030400000000, 0x0000000080452011));
|
||||
|
||||
// Hexadecimal chars in dotted decimal part
|
||||
CHECK(ipv6_address::from_string("64:ff9b::12f.100.30.1") == std::nullopt);
|
||||
CHECK(ipv6_address::from_string("64:ff9b::123.10a.30.1") == std::nullopt);
|
||||
CHECK(ipv6_address::from_string("64:ff9b::123.100.3d.1") == std::nullopt);
|
||||
CHECK(ipv6_address::from_string("64:ff9b::12f.100.30.f4") == std::nullopt);
|
||||
|
||||
// Overflow of individual parts of dotted decimal notation
|
||||
CHECK(ipv6_address::from_string("::ffff:456.12.45.30") == std::nullopt);
|
||||
CHECK(ipv6_address::from_string("::ffff:45.256.45.30") == std::nullopt);
|
||||
CHECK(ipv6_address::from_string("::ffff:45.25.677.30") == std::nullopt);
|
||||
CHECK(ipv6_address::from_string("::ffff:123.12.45.301") == std::nullopt);
|
||||
}
|
||||
|
||||
TEST_CASE("operator<")
|
||||
{
|
||||
ipv6_address a(0x0, 0x1);
|
||||
ipv6_address b(0xff00000000000011, 0xee00000000000022);
|
||||
ipv6_address c(0xee00000000000022, 0xee00000000000022);
|
||||
ipv6_address d(0xee00000000000022, 0xff00000000000011);
|
||||
|
||||
CHECK(a <= a);
|
||||
CHECK(a < b);
|
||||
CHECK(a < c);
|
||||
CHECK(a < d);
|
||||
CHECK(b >= b);
|
||||
CHECK(b > c);
|
||||
CHECK(b > d);
|
||||
CHECK(c < d);
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,55 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/config.hpp>
|
||||
#include <cppcoro/net/ipv6_endpoint.hpp>
|
||||
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("ipv6_endpoint");
|
||||
|
||||
using namespace cppcoro::net;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr bool isMsvc15_5X86Optimised =
|
||||
#if CPPCORO_COMPILER_MSVC && CPPCORO_CPU_X86 && _MSC_VER == 1912 && defined(CPPCORO_RELEASE_OPTIMISED)
|
||||
true;
|
||||
#else
|
||||
false;
|
||||
#endif
|
||||
}
|
||||
|
||||
// BUG: MSVC 15.5 x86 optimised builds generates bad code
|
||||
TEST_CASE("to_string" * doctest::skip{ isMsvc15_5X86Optimised })
|
||||
{
|
||||
CHECK(ipv6_endpoint{ ipv6_address{ 0x20010db885a30000, 0x00008a2e03707334 }, 80 }.to_string() ==
|
||||
"[2001:db8:85a3::8a2e:370:7334]:80");
|
||||
}
|
||||
|
||||
// BUG: MSVC 15.5 x86 optimised builds generates bad code
|
||||
TEST_CASE("from_string" * doctest::skip{ isMsvc15_5X86Optimised })
|
||||
{
|
||||
CHECK(ipv6_endpoint::from_string("") == std::nullopt);
|
||||
CHECK(ipv6_endpoint::from_string(" ") == std::nullopt);
|
||||
CHECK(ipv6_endpoint::from_string("asdf") == std::nullopt);
|
||||
CHECK(ipv6_endpoint::from_string("100:100") == std::nullopt);
|
||||
CHECK(ipv6_endpoint::from_string("100.10.200.20:100") == std::nullopt);
|
||||
CHECK(ipv6_endpoint::from_string("2001:0db8:85a3:0000:0000:8a2e:0370:7334") == std::nullopt);
|
||||
CHECK(ipv6_endpoint::from_string("[2001:0db8:85a3:0000:0000:8a2e:0370:7334") == std::nullopt);
|
||||
CHECK(ipv6_endpoint::from_string("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]") == std::nullopt);
|
||||
CHECK(ipv6_endpoint::from_string("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:") == std::nullopt);
|
||||
CHECK(ipv6_endpoint::from_string("[2001:0db8:85a3:0000:0000:8a2e:0370:7334] :123") == std::nullopt);
|
||||
CHECK(ipv6_endpoint::from_string("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:65536") == std::nullopt);
|
||||
CHECK(ipv6_endpoint::from_string("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:6553600") == std::nullopt);
|
||||
|
||||
CHECK(ipv6_endpoint::from_string("[::]:0") == ipv6_endpoint{});
|
||||
CHECK(ipv6_endpoint::from_string("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:80") ==
|
||||
ipv6_endpoint{ ipv6_address{ 0x20010db885a30000, 0x00008a2e03707334 }, 80 });
|
||||
CHECK(ipv6_endpoint::from_string("[2001:0db8:85a3:0000:0000:8a2e:0370:7334]:65535") ==
|
||||
ipv6_endpoint{ ipv6_address{ 0x20010db885a30000, 0x00008a2e03707334 }, 65535 });
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,7 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
@@ -0,0 +1,205 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/multi_producer_sequencer.hpp>
|
||||
|
||||
#include <cppcoro/config.hpp>
|
||||
#include <cppcoro/sequence_barrier.hpp>
|
||||
#include <cppcoro/sequence_traits.hpp>
|
||||
#include <cppcoro/on_scope_exit.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
#include <cppcoro/when_all.hpp>
|
||||
#include <cppcoro/task.hpp>
|
||||
#include <cppcoro/static_thread_pool.hpp>
|
||||
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
|
||||
#include <ostream>
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
DOCTEST_TEST_SUITE_BEGIN("multi_producer_sequencer");
|
||||
|
||||
using namespace cppcoro;
|
||||
|
||||
namespace
|
||||
{
|
||||
task<> one_at_a_time_producer(
|
||||
static_thread_pool& tp,
|
||||
multi_producer_sequencer<std::size_t>& sequencer,
|
||||
std::uint64_t buffer[],
|
||||
std::uint64_t iterationCount)
|
||||
{
|
||||
if (iterationCount == 0) co_return;
|
||||
|
||||
co_await tp.schedule();
|
||||
|
||||
const std::size_t bufferSize = sequencer.buffer_size();
|
||||
const std::size_t mask = bufferSize - 1;
|
||||
|
||||
std::uint64_t i = 0;
|
||||
while (i < iterationCount)
|
||||
{
|
||||
auto seq = co_await sequencer.claim_one(tp);
|
||||
buffer[seq & mask] = ++i;
|
||||
sequencer.publish(seq);
|
||||
}
|
||||
|
||||
auto finalSeq = co_await sequencer.claim_one(tp);
|
||||
buffer[finalSeq & mask] = 0;
|
||||
sequencer.publish(finalSeq);
|
||||
}
|
||||
|
||||
task<> batch_producer(
|
||||
static_thread_pool& tp,
|
||||
multi_producer_sequencer<std::size_t>& sequencer,
|
||||
std::uint64_t buffer[],
|
||||
std::uint64_t iterationCount,
|
||||
std::size_t maxBatchSize)
|
||||
{
|
||||
const std::size_t bufferSize = sequencer.buffer_size();
|
||||
|
||||
std::uint64_t i = 0;
|
||||
while (i < iterationCount)
|
||||
{
|
||||
const std::size_t batchSize = static_cast<std::size_t>(
|
||||
std::min<std::uint64_t>(maxBatchSize, iterationCount - i));
|
||||
auto sequences = co_await sequencer.claim_up_to(batchSize, tp);
|
||||
for (auto seq : sequences)
|
||||
{
|
||||
buffer[seq % bufferSize] = ++i;
|
||||
}
|
||||
sequencer.publish(sequences);
|
||||
}
|
||||
|
||||
auto finalSeq = co_await sequencer.claim_one(tp);
|
||||
buffer[finalSeq % bufferSize] = 0;
|
||||
sequencer.publish(finalSeq);
|
||||
}
|
||||
|
||||
task<std::uint64_t> consumer(
|
||||
static_thread_pool& tp,
|
||||
const multi_producer_sequencer<std::size_t>& sequencer,
|
||||
sequence_barrier<std::size_t>& readBarrier,
|
||||
const std::uint64_t buffer[],
|
||||
std::uint32_t producerCount)
|
||||
{
|
||||
co_await tp.schedule();
|
||||
|
||||
const std::size_t mask = sequencer.buffer_size() - 1;
|
||||
|
||||
std::uint64_t sum = 0;
|
||||
|
||||
std::uint32_t endCount = 0;
|
||||
std::size_t nextToRead = 0;
|
||||
do
|
||||
{
|
||||
std::size_t available = co_await sequencer.wait_until_published(nextToRead, nextToRead - 1, tp);
|
||||
do
|
||||
{
|
||||
const auto& value = buffer[nextToRead & mask];
|
||||
sum += value;
|
||||
|
||||
// Zero value is sentinel that indicates the end of one of the streams.
|
||||
const bool isEndOfStream = value == 0;
|
||||
endCount += isEndOfStream ? 1 : 0;
|
||||
} while (nextToRead++ != available);
|
||||
|
||||
// Notify that we've finished processing up to 'available'.
|
||||
readBarrier.publish(available);
|
||||
} while (endCount < producerCount);
|
||||
|
||||
co_return sum;
|
||||
}
|
||||
}
|
||||
|
||||
DOCTEST_TEST_CASE("two producers (batch) / single consumer")
|
||||
{
|
||||
static_thread_pool tp{ 3 };
|
||||
|
||||
// Allow time for threads to start up.
|
||||
using namespace std::chrono_literals;
|
||||
std::this_thread::sleep_for(1ms);
|
||||
|
||||
constexpr std::size_t batchSize = 10;
|
||||
constexpr std::size_t bufferSize = 16384;
|
||||
|
||||
sequence_barrier<std::size_t> readBarrier;
|
||||
multi_producer_sequencer<std::size_t> sequencer(readBarrier, bufferSize);
|
||||
|
||||
constexpr std::uint64_t iterationCount = 1'000'000;
|
||||
|
||||
std::uint64_t buffer[bufferSize];
|
||||
|
||||
auto startTime = std::chrono::high_resolution_clock::now();
|
||||
|
||||
constexpr std::uint32_t producerCount = 2;
|
||||
auto result = std::get<0>(sync_wait(when_all(
|
||||
consumer(tp, sequencer, readBarrier, buffer, producerCount),
|
||||
batch_producer(tp, sequencer, buffer, iterationCount, batchSize),
|
||||
batch_producer(tp, sequencer, buffer, iterationCount, batchSize))));
|
||||
|
||||
auto endTime = std::chrono::high_resolution_clock::now();
|
||||
|
||||
auto totalTimeInNs = std::chrono::duration_cast<std::chrono::nanoseconds>(endTime - startTime).count();
|
||||
|
||||
MESSAGE(
|
||||
"Producers = " << producerCount
|
||||
<< ", BatchSize = " << batchSize
|
||||
<< ", MessagesPerProducer = " << iterationCount
|
||||
<< ", TotalTime = " << totalTimeInNs/1000 << "us"
|
||||
<< ", TimePerMessage = " << totalTimeInNs/double(iterationCount * producerCount) << "ns"
|
||||
<< ", MessagesPerSecond = " << 1'000'000'000 * (producerCount * iterationCount) / totalTimeInNs);
|
||||
|
||||
constexpr std::uint64_t expectedResult =
|
||||
producerCount * std::uint64_t(iterationCount) * std::uint64_t(iterationCount + 1) / 2;
|
||||
|
||||
CHECK(result == expectedResult);
|
||||
}
|
||||
|
||||
DOCTEST_TEST_CASE("two producers (single) / single consumer")
|
||||
{
|
||||
static_thread_pool tp{ 3 };
|
||||
|
||||
// Allow time for threads to start up.
|
||||
using namespace std::chrono_literals;
|
||||
std::this_thread::sleep_for(1ms);
|
||||
|
||||
constexpr std::size_t bufferSize = 16384;
|
||||
|
||||
sequence_barrier<std::size_t> readBarrier;
|
||||
multi_producer_sequencer<std::size_t> sequencer(readBarrier, bufferSize);
|
||||
|
||||
constexpr std::uint64_t iterationCount = 1'000'000;
|
||||
|
||||
std::uint64_t buffer[bufferSize];
|
||||
|
||||
auto startTime = std::chrono::high_resolution_clock::now();
|
||||
|
||||
constexpr std::uint32_t producerCount = 2;
|
||||
auto result = std::get<0>(sync_wait(when_all(
|
||||
consumer(tp, sequencer, readBarrier, buffer, producerCount),
|
||||
one_at_a_time_producer(tp, sequencer, buffer, iterationCount),
|
||||
one_at_a_time_producer(tp, sequencer, buffer, iterationCount))));
|
||||
|
||||
auto endTime = std::chrono::high_resolution_clock::now();
|
||||
|
||||
auto totalTimeInNs = std::chrono::duration_cast<std::chrono::nanoseconds>(endTime - startTime).count();
|
||||
|
||||
MESSAGE(
|
||||
"Producers = " << producerCount
|
||||
<< ", NoBatch"
|
||||
<< ", MessagesPerProducer = " << iterationCount
|
||||
<< ", TotalTime = " << totalTimeInNs / 1000 << "us"
|
||||
<< ", TimePerMessage = " << totalTimeInNs / double(iterationCount * producerCount) << "ns"
|
||||
<< ", MessagesPerSecond = " << 1'000'000'000 * (producerCount * iterationCount) / totalTimeInNs);
|
||||
|
||||
constexpr std::uint64_t expectedResult =
|
||||
producerCount * std::uint64_t(iterationCount) * std::uint64_t(iterationCount + 1) / 2;
|
||||
|
||||
CHECK(result == expectedResult);
|
||||
}
|
||||
|
||||
DOCTEST_TEST_SUITE_END();
|
||||
@@ -0,0 +1,424 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/recursive_generator.hpp>
|
||||
#include <cppcoro/on_scope_exit.hpp>
|
||||
#include <cppcoro/fmap.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <algorithm>
|
||||
|
||||
#include <ostream>
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("recursive_generator");
|
||||
|
||||
using cppcoro::recursive_generator;
|
||||
|
||||
TEST_CASE("default constructed recursive_generator is empty")
|
||||
{
|
||||
recursive_generator<std::uint32_t> ints;
|
||||
CHECK(ints.begin() == ints.end());
|
||||
}
|
||||
|
||||
TEST_CASE("non-recursive use of recursive_generator")
|
||||
{
|
||||
auto f = []() -> recursive_generator<float>
|
||||
{
|
||||
co_yield 1.0f;
|
||||
co_yield 2.0f;
|
||||
};
|
||||
|
||||
auto gen = f();
|
||||
auto iter = gen.begin();
|
||||
CHECK(*iter == 1.0f);
|
||||
++iter;
|
||||
CHECK(*iter == 2.0f);
|
||||
++iter;
|
||||
CHECK(iter == gen.end());
|
||||
}
|
||||
|
||||
TEST_CASE("throw before first yield")
|
||||
{
|
||||
class MyException : public std::exception {};
|
||||
|
||||
auto f = []() -> recursive_generator<std::uint32_t>
|
||||
{
|
||||
throw MyException{};
|
||||
co_return;
|
||||
};
|
||||
|
||||
auto gen = f();
|
||||
try
|
||||
{
|
||||
auto iter = gen.begin();
|
||||
CHECK(false);
|
||||
}
|
||||
catch (MyException)
|
||||
{
|
||||
CHECK(true);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("throw after first yield")
|
||||
{
|
||||
class MyException : public std::exception {};
|
||||
|
||||
auto f = []() -> recursive_generator<std::uint32_t>
|
||||
{
|
||||
co_yield 1;
|
||||
throw MyException{};
|
||||
};
|
||||
|
||||
auto gen = f();
|
||||
auto iter = gen.begin();
|
||||
CHECK(*iter == 1u);
|
||||
try
|
||||
{
|
||||
++iter;
|
||||
CHECK(false);
|
||||
}
|
||||
catch (MyException)
|
||||
{
|
||||
CHECK(true);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("generator doesn't start executing until begin is called")
|
||||
{
|
||||
bool reachedA = false;
|
||||
bool reachedB = false;
|
||||
bool reachedC = false;
|
||||
auto f = [&]() -> recursive_generator<std::uint32_t>
|
||||
{
|
||||
reachedA = true;
|
||||
co_yield 1;
|
||||
reachedB = true;
|
||||
co_yield 2;
|
||||
reachedC = true;
|
||||
};
|
||||
|
||||
auto gen = f();
|
||||
CHECK(!reachedA);
|
||||
auto iter = gen.begin();
|
||||
CHECK(reachedA);
|
||||
CHECK(!reachedB);
|
||||
CHECK(*iter == 1u);
|
||||
++iter;
|
||||
CHECK(reachedB);
|
||||
CHECK(!reachedC);
|
||||
CHECK(*iter == 2u);
|
||||
++iter;
|
||||
CHECK(reachedC);
|
||||
CHECK(iter == gen.end());
|
||||
}
|
||||
|
||||
TEST_CASE("destroying generator before completion destructs objects on stack")
|
||||
{
|
||||
bool destructed = false;
|
||||
bool completed = false;
|
||||
auto f = [&]() -> recursive_generator<std::uint32_t>
|
||||
{
|
||||
auto onExit = cppcoro::on_scope_exit([&]
|
||||
{
|
||||
destructed = true;
|
||||
});
|
||||
|
||||
co_yield 1;
|
||||
co_yield 2;
|
||||
completed = true;
|
||||
};
|
||||
|
||||
{
|
||||
auto g = f();
|
||||
auto it = g.begin();
|
||||
auto itEnd = g.end();
|
||||
CHECK(*it == 1u);
|
||||
CHECK(!destructed);
|
||||
}
|
||||
|
||||
CHECK(!completed);
|
||||
CHECK(destructed);
|
||||
}
|
||||
|
||||
TEST_CASE("simple recursive yield")
|
||||
{
|
||||
auto f = [](int n, auto& f) -> recursive_generator<const std::uint32_t>
|
||||
{
|
||||
co_yield n;
|
||||
if (n > 0)
|
||||
{
|
||||
co_yield f(n - 1, f);
|
||||
co_yield n;
|
||||
}
|
||||
};
|
||||
|
||||
auto f2 = [&f](int n)
|
||||
{
|
||||
return f(n, f);
|
||||
};
|
||||
|
||||
{
|
||||
auto gen = f2(1);
|
||||
auto iter = gen.begin();
|
||||
CHECK(*iter == 1u);
|
||||
++iter;
|
||||
CHECK(*iter == 0u);
|
||||
++iter;
|
||||
CHECK(*iter == 1u);
|
||||
++iter;
|
||||
CHECK(iter == gen.end());
|
||||
}
|
||||
|
||||
{
|
||||
auto gen = f2(2);
|
||||
auto iter = gen.begin();
|
||||
CHECK(*iter == 2u);
|
||||
++iter;
|
||||
CHECK(*iter == 1u);
|
||||
++iter;
|
||||
CHECK(*iter == 0u);
|
||||
++iter;
|
||||
CHECK(*iter == 1u);
|
||||
++iter;
|
||||
CHECK(*iter == 2u);
|
||||
++iter;
|
||||
CHECK(iter == gen.end());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("nested yield that yields nothing")
|
||||
{
|
||||
auto f = []() -> recursive_generator<std::uint32_t>
|
||||
{
|
||||
co_return;
|
||||
};
|
||||
|
||||
auto g = [&f]() -> recursive_generator<std::uint32_t>
|
||||
{
|
||||
co_yield 1;
|
||||
co_yield f();
|
||||
co_yield 2;
|
||||
};
|
||||
|
||||
auto gen = g();
|
||||
auto iter = gen.begin();
|
||||
CHECK(*iter == 1u);
|
||||
++iter;
|
||||
CHECK(*iter == 2u);
|
||||
++iter;
|
||||
CHECK(iter == gen.end());
|
||||
}
|
||||
|
||||
TEST_CASE("exception thrown from recursive call can be caught by caller")
|
||||
{
|
||||
class SomeException : public std::exception {};
|
||||
|
||||
auto f = [](std::uint32_t depth, auto&& f) -> recursive_generator<std::uint32_t>
|
||||
{
|
||||
if (depth == 1u)
|
||||
{
|
||||
throw SomeException{};
|
||||
}
|
||||
|
||||
co_yield 1;
|
||||
|
||||
try
|
||||
{
|
||||
co_yield f(1, f);
|
||||
}
|
||||
catch (SomeException)
|
||||
{
|
||||
}
|
||||
|
||||
co_yield 2;
|
||||
};
|
||||
|
||||
auto gen = f(0, f);
|
||||
auto iter = gen.begin();
|
||||
CHECK(*iter == 1u);
|
||||
++iter;
|
||||
CHECK(*iter == 2u);
|
||||
++iter;
|
||||
CHECK(iter == gen.end());
|
||||
}
|
||||
|
||||
TEST_CASE("exceptions thrown from nested call can be caught by caller")
|
||||
{
|
||||
class SomeException : public std::exception {};
|
||||
|
||||
auto f = [](std::uint32_t depth, auto&& f) -> recursive_generator<std::uint32_t>
|
||||
{
|
||||
if (depth == 4u)
|
||||
{
|
||||
throw SomeException{};
|
||||
}
|
||||
else if (depth == 3u)
|
||||
{
|
||||
co_yield 3;
|
||||
|
||||
try
|
||||
{
|
||||
co_yield f(4, f);
|
||||
}
|
||||
catch (SomeException)
|
||||
{
|
||||
}
|
||||
|
||||
co_yield 33;
|
||||
|
||||
throw SomeException{};
|
||||
}
|
||||
else if (depth == 2u)
|
||||
{
|
||||
bool caught = false;
|
||||
try
|
||||
{
|
||||
co_yield f(3, f);
|
||||
}
|
||||
catch (SomeException)
|
||||
{
|
||||
caught = true;
|
||||
}
|
||||
|
||||
if (caught)
|
||||
{
|
||||
co_yield 2;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
co_yield 1;
|
||||
co_yield f(2, f);
|
||||
co_yield f(3, f);
|
||||
}
|
||||
};
|
||||
|
||||
auto gen = f(1, f);
|
||||
auto iter = gen.begin();
|
||||
CHECK(*iter == 1u);
|
||||
++iter;
|
||||
CHECK(*iter == 3u);
|
||||
++iter;
|
||||
CHECK(*iter == 33u);
|
||||
++iter;
|
||||
CHECK(*iter == 2u);
|
||||
++iter;
|
||||
CHECK(*iter == 3u);
|
||||
++iter;
|
||||
CHECK(*iter == 33u);
|
||||
try
|
||||
{
|
||||
++iter;
|
||||
CHECK(false);
|
||||
}
|
||||
catch (SomeException)
|
||||
{
|
||||
}
|
||||
|
||||
CHECK(iter == gen.end());
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
recursive_generator<std::uint32_t> iterate_range(std::uint32_t begin, std::uint32_t end)
|
||||
{
|
||||
if ((end - begin) <= 10u)
|
||||
{
|
||||
for (std::uint32_t i = begin; i < end; ++i)
|
||||
{
|
||||
co_yield i;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::uint32_t mid = begin + (end - begin) / 2;
|
||||
co_yield iterate_range(begin, mid);
|
||||
co_yield iterate_range(mid, end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("recursive iteration performance")
|
||||
{
|
||||
const std::uint32_t count = 100000;
|
||||
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
std::uint64_t sum = 0;
|
||||
for (auto i : iterate_range(0, count))
|
||||
{
|
||||
sum += i;
|
||||
}
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
|
||||
CHECK(sum == (std::uint64_t(count) * (count - 1)) / 2);
|
||||
|
||||
const auto timeTakenUs = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
|
||||
MESSAGE("Range iteration of " << count << "elements took " << timeTakenUs << "us");
|
||||
}
|
||||
|
||||
TEST_CASE("usage in standard algorithms")
|
||||
{
|
||||
{
|
||||
auto a = iterate_range(5, 30);
|
||||
auto b = iterate_range(5, 30);
|
||||
CHECK(std::equal(a.begin(), a.end(), b.begin(), b.end()));
|
||||
}
|
||||
|
||||
{
|
||||
auto a = iterate_range(5, 30);
|
||||
auto b = iterate_range(5, 300);
|
||||
CHECK(!std::equal(a.begin(), a.end(), b.begin(), b.end()));
|
||||
}
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
recursive_generator<int> range(int start, int end)
|
||||
{
|
||||
while (start < end)
|
||||
{
|
||||
co_yield start++;
|
||||
}
|
||||
}
|
||||
|
||||
recursive_generator<int> range_chunks(int start, int end, int runLength, int stride)
|
||||
{
|
||||
while (start < end)
|
||||
{
|
||||
co_yield range(start, std::min(end, start + runLength));
|
||||
start += stride;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("fmap operator")
|
||||
{
|
||||
// 0, 1, 2, 3, 4, 10, 11, 12, 13, 14, 20, 21, 22, 23, 24
|
||||
cppcoro::generator<int> gen = range_chunks(0, 30, 5, 10)
|
||||
| cppcoro::fmap([](int x) { return x * 3; });
|
||||
|
||||
auto it = gen.begin();
|
||||
CHECK(*it == 0);
|
||||
CHECK(*++it == 3);
|
||||
CHECK(*++it == 6);
|
||||
CHECK(*++it == 9);
|
||||
CHECK(*++it == 12);
|
||||
CHECK(*++it == 30);
|
||||
CHECK(*++it == 33);
|
||||
CHECK(*++it == 36);
|
||||
CHECK(*++it == 39);
|
||||
CHECK(*++it == 42);
|
||||
CHECK(*++it == 60);
|
||||
CHECK(*++it == 63);
|
||||
CHECK(*++it == 66);
|
||||
CHECK(*++it == 69);
|
||||
CHECK(*++it == 72);
|
||||
CHECK(++it == gen.end());
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,290 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/schedule_on.hpp>
|
||||
#include <cppcoro/resume_on.hpp>
|
||||
#include <cppcoro/io_service.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
#include <cppcoro/when_all_ready.hpp>
|
||||
#include <cppcoro/on_scope_exit.hpp>
|
||||
#include <cppcoro/fmap.hpp>
|
||||
|
||||
#include "io_service_fixture.hpp"
|
||||
|
||||
#include <ostream>
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("schedule/resume_on");
|
||||
|
||||
TEST_CASE_FIXTURE(io_service_fixture, "schedule_on task<> function")
|
||||
{
|
||||
auto mainThreadId = std::this_thread::get_id();
|
||||
|
||||
std::thread::id ioThreadId;
|
||||
|
||||
auto start = [&]() -> cppcoro::task<>
|
||||
{
|
||||
ioThreadId = std::this_thread::get_id();
|
||||
CHECK(ioThreadId != mainThreadId);
|
||||
co_return;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait([&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(std::this_thread::get_id() == mainThreadId);
|
||||
|
||||
co_await schedule_on(io_service(), start());
|
||||
|
||||
// TODO: Uncomment this check once the implementation of task<T>
|
||||
// guarantees that the continuation will resume on the same thread
|
||||
// that the task completed on. Currently it's possible to resume on
|
||||
// the thread that launched the task if it completes on another thread
|
||||
// before the current thread could attach the continuation after it
|
||||
// suspended. See cppcoro issue #79.
|
||||
//
|
||||
// The long-term solution here is to use the symmetric-transfer capability
|
||||
// to avoid the use of atomics and races, but we're still waiting for MSVC to
|
||||
// implement this (doesn't seem to be implemented as of VS 2017.8 Preview 5)
|
||||
//CHECK(std::this_thread::get_id() == ioThreadId);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(io_service_fixture, "schedule_on async_generator<> function")
|
||||
{
|
||||
auto mainThreadId = std::this_thread::get_id();
|
||||
|
||||
std::thread::id ioThreadId;
|
||||
|
||||
auto makeSequence = [&]() -> cppcoro::async_generator<int>
|
||||
{
|
||||
ioThreadId = std::this_thread::get_id();
|
||||
CHECK(ioThreadId != mainThreadId);
|
||||
|
||||
co_yield 1;
|
||||
|
||||
CHECK(std::this_thread::get_id() == ioThreadId);
|
||||
|
||||
co_yield 2;
|
||||
|
||||
CHECK(std::this_thread::get_id() == ioThreadId);
|
||||
|
||||
co_yield 3;
|
||||
|
||||
CHECK(std::this_thread::get_id() == ioThreadId);
|
||||
|
||||
co_return;
|
||||
};
|
||||
|
||||
cppcoro::io_service otherIoService;
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(std::this_thread::get_id() == mainThreadId);
|
||||
|
||||
auto seq = schedule_on(io_service(), makeSequence());
|
||||
|
||||
int expected = 1;
|
||||
for (auto iter = co_await seq.begin(); iter != seq.end(); co_await ++iter)
|
||||
{
|
||||
int value = *iter;
|
||||
CHECK(value == expected++);
|
||||
|
||||
// Transfer exection back to main thread before
|
||||
// awaiting next item in the loop to chck that
|
||||
// the generator is resumed on io_service() thread.
|
||||
co_await otherIoService.schedule();
|
||||
}
|
||||
|
||||
otherIoService.stop();
|
||||
}(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
otherIoService.process_events();
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(io_service_fixture, "resume_on task<> function")
|
||||
{
|
||||
auto mainThreadId = std::this_thread::get_id();
|
||||
|
||||
auto start = [&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(std::this_thread::get_id() == mainThreadId);
|
||||
co_return;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait([&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(std::this_thread::get_id() == mainThreadId);
|
||||
|
||||
co_await resume_on(io_service(), start());
|
||||
|
||||
// NOTE: This check could potentially spuriously fail with the current
|
||||
// implementation of task<T>. See cppcoro issue #79.
|
||||
CHECK(std::this_thread::get_id() != mainThreadId);
|
||||
}());
|
||||
}
|
||||
|
||||
constexpr bool isMsvc15_4X86Optimised =
|
||||
#if defined(_MSC_VER) && _MSC_VER == 1911 && defined(_M_IX86) && !defined(_DEBUG)
|
||||
true;
|
||||
#else
|
||||
false;
|
||||
#endif
|
||||
|
||||
// Disable under MSVC 15.4 X86 Optimised due to presumed compiler bug that causes
|
||||
// an access violation. Seems to be fixed under MSVC 15.5.
|
||||
TEST_CASE_FIXTURE(io_service_fixture, "resume_on async_generator<> function"
|
||||
* doctest::skip{ isMsvc15_4X86Optimised })
|
||||
{
|
||||
auto mainThreadId = std::this_thread::get_id();
|
||||
|
||||
std::thread::id ioThreadId;
|
||||
|
||||
auto makeSequence = [&]() -> cppcoro::async_generator<int>
|
||||
{
|
||||
co_await io_service().schedule();
|
||||
|
||||
ioThreadId = std::this_thread::get_id();
|
||||
|
||||
CHECK(ioThreadId != mainThreadId);
|
||||
|
||||
co_yield 1;
|
||||
|
||||
co_yield 2;
|
||||
|
||||
co_await io_service().schedule();
|
||||
|
||||
co_yield 3;
|
||||
|
||||
co_await io_service().schedule();
|
||||
|
||||
co_return;
|
||||
};
|
||||
|
||||
cppcoro::io_service otherIoService;
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
auto stopOnExit = cppcoro::on_scope_exit([&] { otherIoService.stop(); });
|
||||
|
||||
CHECK(std::this_thread::get_id() == mainThreadId);
|
||||
|
||||
auto seq = resume_on(otherIoService, makeSequence());
|
||||
|
||||
int expected = 1;
|
||||
for (auto iter = co_await seq.begin(); iter != seq.end(); co_await ++iter)
|
||||
{
|
||||
int value = *iter;
|
||||
// Every time we receive a value it should be on our requested
|
||||
// scheduler (ie. main thread)
|
||||
CHECK(std::this_thread::get_id() == mainThreadId);
|
||||
CHECK(value == expected++);
|
||||
|
||||
// Occasionally transfer execution to a different thread before
|
||||
// awaiting next element.
|
||||
if (value == 2)
|
||||
{
|
||||
co_await io_service().schedule();
|
||||
}
|
||||
}
|
||||
|
||||
otherIoService.stop();
|
||||
}(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
otherIoService.process_events();
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(io_service_fixture, "schedule_on task<> pipe syntax")
|
||||
{
|
||||
auto mainThreadId = std::this_thread::get_id();
|
||||
|
||||
auto makeTask = [&]() -> cppcoro::task<int>
|
||||
{
|
||||
CHECK(std::this_thread::get_id() != mainThreadId);
|
||||
co_return 123;
|
||||
};
|
||||
|
||||
auto triple = [&](int x)
|
||||
{
|
||||
CHECK(std::this_thread::get_id() != mainThreadId);
|
||||
return x * 3;
|
||||
};
|
||||
|
||||
CHECK(cppcoro::sync_wait(makeTask() | schedule_on(io_service())) == 123);
|
||||
|
||||
// Shouldn't matter where in sequence schedule_on() appears since it applies
|
||||
// at the start of the pipeline (ie. before first task starts).
|
||||
CHECK(cppcoro::sync_wait(makeTask() | schedule_on(io_service()) | cppcoro::fmap(triple)) == 369);
|
||||
CHECK(cppcoro::sync_wait(makeTask() | cppcoro::fmap(triple) | schedule_on(io_service())) == 369);
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(io_service_fixture, "resume_on task<> pipe syntax")
|
||||
{
|
||||
auto mainThreadId = std::this_thread::get_id();
|
||||
|
||||
auto makeTask = [&]() -> cppcoro::task<int>
|
||||
{
|
||||
CHECK(std::this_thread::get_id() == mainThreadId);
|
||||
co_return 123;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait([&]() -> cppcoro::task<>
|
||||
{
|
||||
cppcoro::task<int> t = makeTask() | cppcoro::resume_on(io_service());
|
||||
CHECK(co_await t == 123);
|
||||
CHECK(std::this_thread::get_id() != mainThreadId);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_CASE_FIXTURE(io_service_fixture, "resume_on task<> pipe syntax multiple uses")
|
||||
{
|
||||
auto mainThreadId = std::this_thread::get_id();
|
||||
|
||||
auto makeTask = [&]() -> cppcoro::task<int>
|
||||
{
|
||||
CHECK(std::this_thread::get_id() == mainThreadId);
|
||||
co_return 123;
|
||||
};
|
||||
|
||||
auto triple = [&](int x)
|
||||
{
|
||||
CHECK(std::this_thread::get_id() != mainThreadId);
|
||||
return x * 3;
|
||||
};
|
||||
|
||||
cppcoro::io_service otherIoService;
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
auto stopOnExit = cppcoro::on_scope_exit([&] { otherIoService.stop(); });
|
||||
|
||||
CHECK(std::this_thread::get_id() == mainThreadId);
|
||||
|
||||
cppcoro::task<int> t =
|
||||
makeTask()
|
||||
| cppcoro::resume_on(io_service())
|
||||
| cppcoro::fmap(triple)
|
||||
| cppcoro::resume_on(otherIoService);
|
||||
|
||||
CHECK(co_await t == 369);
|
||||
|
||||
CHECK(std::this_thread::get_id() == mainThreadId);
|
||||
}(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
otherIoService.process_events();
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,213 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/sequence_barrier.hpp>
|
||||
|
||||
#include <cppcoro/config.hpp>
|
||||
#include <cppcoro/task.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
#include <cppcoro/when_all.hpp>
|
||||
#include <cppcoro/static_thread_pool.hpp>
|
||||
#include <cppcoro/inline_scheduler.hpp>
|
||||
|
||||
#include <stdio.h>
|
||||
#include <thread>
|
||||
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
DOCTEST_TEST_SUITE_BEGIN("sequence_barrier");
|
||||
|
||||
using namespace cppcoro;
|
||||
|
||||
DOCTEST_TEST_CASE("default construction")
|
||||
{
|
||||
sequence_barrier<std::uint32_t> barrier;
|
||||
CHECK(barrier.last_published() == sequence_traits<std::uint32_t>::initial_sequence);
|
||||
barrier.publish(3);
|
||||
CHECK(barrier.last_published() == 3);
|
||||
}
|
||||
|
||||
DOCTEST_TEST_CASE("constructing with initial sequence number")
|
||||
{
|
||||
sequence_barrier<std::uint64_t> barrier{ 100 };
|
||||
CHECK(barrier.last_published() == 100);
|
||||
}
|
||||
|
||||
DOCTEST_TEST_CASE("wait_until_published single-threaded")
|
||||
{
|
||||
inline_scheduler scheduler;
|
||||
|
||||
sequence_barrier<std::uint32_t> barrier;
|
||||
bool reachedA = false;
|
||||
bool reachedB = false;
|
||||
bool reachedC = false;
|
||||
bool reachedD = false;
|
||||
bool reachedE = false;
|
||||
bool reachedF = false;
|
||||
sync_wait(when_all(
|
||||
[&]() -> task<>
|
||||
{
|
||||
CHECK(co_await barrier.wait_until_published(0, scheduler) == 0);
|
||||
reachedA = true;
|
||||
CHECK(co_await barrier.wait_until_published(1, scheduler) == 1);
|
||||
reachedB = true;
|
||||
CHECK(co_await barrier.wait_until_published(3, scheduler) == 3);
|
||||
reachedC = true;
|
||||
CHECK(co_await barrier.wait_until_published(4, scheduler) == 10);
|
||||
reachedD = true;
|
||||
co_await barrier.wait_until_published(5, scheduler);
|
||||
reachedE = true;
|
||||
co_await barrier.wait_until_published(10, scheduler);
|
||||
reachedF = true;
|
||||
}(),
|
||||
[&]() -> task<>
|
||||
{
|
||||
CHECK(!reachedA);
|
||||
barrier.publish(0);
|
||||
CHECK(reachedA);
|
||||
CHECK(!reachedB);
|
||||
barrier.publish(1);
|
||||
CHECK(reachedB);
|
||||
CHECK(!reachedC);
|
||||
barrier.publish(2);
|
||||
CHECK(!reachedC);
|
||||
barrier.publish(3);
|
||||
CHECK(reachedC);
|
||||
CHECK(!reachedD);
|
||||
barrier.publish(10);
|
||||
CHECK(reachedD);
|
||||
CHECK(reachedE);
|
||||
CHECK(reachedF);
|
||||
co_return;
|
||||
}()));
|
||||
CHECK(reachedF);
|
||||
}
|
||||
|
||||
DOCTEST_TEST_CASE("wait_until_published multiple awaiters")
|
||||
{
|
||||
inline_scheduler scheduler;
|
||||
|
||||
sequence_barrier<std::uint32_t> barrier;
|
||||
bool reachedA = false;
|
||||
bool reachedB = false;
|
||||
bool reachedC = false;
|
||||
bool reachedD = false;
|
||||
bool reachedE = false;
|
||||
sync_wait(when_all(
|
||||
[&]() -> task<>
|
||||
{
|
||||
CHECK(co_await barrier.wait_until_published(0, scheduler) == 0);
|
||||
reachedA = true;
|
||||
CHECK(co_await barrier.wait_until_published(1, scheduler) == 1);
|
||||
reachedB = true;
|
||||
CHECK(co_await barrier.wait_until_published(3, scheduler) == 3);
|
||||
reachedC = true;
|
||||
}(),
|
||||
[&]() -> task<>
|
||||
{
|
||||
CHECK(co_await barrier.wait_until_published(0, scheduler) == 0);
|
||||
reachedD = true;
|
||||
CHECK(co_await barrier.wait_until_published(3, scheduler) == 3);
|
||||
reachedE = true;
|
||||
}(),
|
||||
[&]() -> task<>
|
||||
{
|
||||
CHECK(!reachedA);
|
||||
CHECK(!reachedD);
|
||||
barrier.publish(0);
|
||||
CHECK(reachedA);
|
||||
CHECK(reachedD);
|
||||
CHECK(!reachedB);
|
||||
CHECK(!reachedE);
|
||||
barrier.publish(1);
|
||||
CHECK(reachedB);
|
||||
CHECK(!reachedC);
|
||||
CHECK(!reachedE);
|
||||
barrier.publish(2);
|
||||
CHECK(!reachedC);
|
||||
CHECK(!reachedE);
|
||||
barrier.publish(3);
|
||||
CHECK(reachedC);
|
||||
CHECK(reachedE);
|
||||
co_return;
|
||||
}()));
|
||||
CHECK(reachedC);
|
||||
CHECK(reachedE);
|
||||
}
|
||||
|
||||
DOCTEST_TEST_CASE("multi-threaded usage single consumer")
|
||||
{
|
||||
static_thread_pool tp{ 2 };
|
||||
|
||||
sequence_barrier<std::size_t> writeBarrier;
|
||||
sequence_barrier<std::size_t> readBarrier;
|
||||
|
||||
constexpr std::size_t iterationCount = 1'000'000;
|
||||
|
||||
constexpr std::size_t bufferSize = 256;
|
||||
std::uint64_t buffer[bufferSize];
|
||||
|
||||
auto[result, dummy] = sync_wait(when_all(
|
||||
[&]() -> task<std::uint64_t>
|
||||
{
|
||||
// Consumer
|
||||
std::uint64_t sum = 0;
|
||||
|
||||
bool reachedEnd = false;
|
||||
std::size_t nextToRead = 0;
|
||||
do
|
||||
{
|
||||
std::size_t available = co_await writeBarrier.wait_until_published(nextToRead, tp);
|
||||
do
|
||||
{
|
||||
sum += buffer[nextToRead % bufferSize];
|
||||
} while (nextToRead++ != available);
|
||||
|
||||
// Zero value is sentinel that indicates the end of the stream.
|
||||
reachedEnd = buffer[available % bufferSize] == 0;
|
||||
|
||||
// Notify that we've finished processing up to 'available'.
|
||||
readBarrier.publish(available);
|
||||
} while (!reachedEnd);
|
||||
|
||||
co_return sum;
|
||||
}(),
|
||||
[&]() -> task<>
|
||||
{
|
||||
// Producer
|
||||
std::size_t available = readBarrier.last_published() + bufferSize;
|
||||
for (std::size_t nextToWrite = 0; nextToWrite <= iterationCount; ++nextToWrite)
|
||||
{
|
||||
if (sequence_traits<std::size_t>::precedes(available, nextToWrite))
|
||||
{
|
||||
available = co_await readBarrier.wait_until_published(nextToWrite - bufferSize, tp) + bufferSize;
|
||||
}
|
||||
|
||||
if (nextToWrite == iterationCount)
|
||||
{
|
||||
// Write sentinel (zero) as last element.
|
||||
buffer[nextToWrite % bufferSize] = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write value
|
||||
buffer[nextToWrite % bufferSize] = nextToWrite + 1;
|
||||
}
|
||||
|
||||
// Notify consumer that we've published a new value.
|
||||
writeBarrier.publish(nextToWrite);
|
||||
}
|
||||
}()));
|
||||
|
||||
// Suppress unused variable warning.
|
||||
(void)dummy;
|
||||
|
||||
constexpr std::uint64_t expectedResult =
|
||||
std::uint64_t(iterationCount) * std::uint64_t(iterationCount + 1) / 2;
|
||||
|
||||
CHECK(result == expectedResult);
|
||||
}
|
||||
|
||||
DOCTEST_TEST_SUITE_END();
|
||||
@@ -0,0 +1,248 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/shared_task.hpp>
|
||||
#include <cppcoro/task.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
#include <cppcoro/when_all_ready.hpp>
|
||||
#include <cppcoro/single_consumer_event.hpp>
|
||||
#include <cppcoro/fmap.hpp>
|
||||
|
||||
#include "counted.hpp"
|
||||
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("shared_task");
|
||||
|
||||
TEST_CASE("awaiting default-constructed task throws broken_promise")
|
||||
{
|
||||
cppcoro::sync_wait([]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK_THROWS_AS(co_await cppcoro::shared_task<>{}, const cppcoro::broken_promise&);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_CASE("coroutine doesn't start executing until awaited")
|
||||
{
|
||||
bool startedExecuting = false;
|
||||
auto f = [&]() -> cppcoro::shared_task<>
|
||||
{
|
||||
startedExecuting = true;
|
||||
co_return;
|
||||
};
|
||||
|
||||
auto t = f();
|
||||
|
||||
CHECK(!t.is_ready());
|
||||
CHECK(!startedExecuting);
|
||||
|
||||
cppcoro::sync_wait([](cppcoro::shared_task<> t) -> cppcoro::task<>
|
||||
{
|
||||
co_await t;
|
||||
}(t));
|
||||
|
||||
CHECK(t.is_ready());
|
||||
CHECK(startedExecuting);
|
||||
}
|
||||
|
||||
TEST_CASE("result is destroyed when last reference is destroyed")
|
||||
{
|
||||
counted::reset_counts();
|
||||
|
||||
{
|
||||
auto t = []() -> cppcoro::shared_task<counted>
|
||||
{
|
||||
co_return counted{};
|
||||
}();
|
||||
|
||||
CHECK(counted::active_count() == 0);
|
||||
|
||||
cppcoro::sync_wait(t);
|
||||
|
||||
CHECK(counted::active_count() == 1);
|
||||
}
|
||||
|
||||
CHECK(counted::active_count() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("multiple awaiters")
|
||||
{
|
||||
cppcoro::single_consumer_event event;
|
||||
bool startedExecution = false;
|
||||
auto produce = [&]() -> cppcoro::shared_task<int>
|
||||
{
|
||||
startedExecution = true;
|
||||
co_await event;
|
||||
co_return 1;
|
||||
};
|
||||
|
||||
auto consume = [](cppcoro::shared_task<int> t) -> cppcoro::task<>
|
||||
{
|
||||
int result = co_await t;
|
||||
CHECK(result == 1);
|
||||
};
|
||||
|
||||
auto sharedTask = produce();
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
consume(sharedTask),
|
||||
consume(sharedTask),
|
||||
consume(sharedTask),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
event.set();
|
||||
CHECK(sharedTask.is_ready());
|
||||
co_return;
|
||||
}()));
|
||||
|
||||
CHECK(sharedTask.is_ready());
|
||||
}
|
||||
|
||||
TEST_CASE("waiting on shared_task in loop doesn't cause stack-overflow")
|
||||
{
|
||||
// This test checks that awaiting a shared_task that completes
|
||||
// synchronously doesn't recursively resume the awaiter inside the
|
||||
// call to start executing the task. If it were to do this then we'd
|
||||
// expect that this test would result in failure due to stack-overflow.
|
||||
|
||||
auto completesSynchronously = []() -> cppcoro::shared_task<int>
|
||||
{
|
||||
co_return 1;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait([&]() -> cppcoro::task<>
|
||||
{
|
||||
int result = 0;
|
||||
for (int i = 0; i < 1'000'000; ++i)
|
||||
{
|
||||
result += co_await completesSynchronously();
|
||||
}
|
||||
CHECK(result == 1'000'000);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_CASE("make_shared_task")
|
||||
{
|
||||
bool startedExecution = false;
|
||||
|
||||
auto f = [&]() -> cppcoro::task<std::string>
|
||||
{
|
||||
startedExecution = false;
|
||||
co_return "test";
|
||||
};
|
||||
|
||||
auto t = f();
|
||||
|
||||
cppcoro::shared_task<std::string> sharedT =
|
||||
cppcoro::make_shared_task(std::move(t));
|
||||
|
||||
CHECK(!sharedT.is_ready());
|
||||
CHECK(!startedExecution);
|
||||
|
||||
auto consume = [](cppcoro::shared_task<std::string> t) -> cppcoro::task<>
|
||||
{
|
||||
auto x = co_await std::move(t);
|
||||
CHECK(x == "test");
|
||||
};
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
consume(sharedT),
|
||||
consume(sharedT)));
|
||||
}
|
||||
|
||||
TEST_CASE("make_shared_task of void"
|
||||
* doctest::description{ "Tests that workaround for 'co_return <void-expr>' bug is operational if required" })
|
||||
{
|
||||
bool startedExecution = false;
|
||||
|
||||
auto f = [&]() -> cppcoro::task<>
|
||||
{
|
||||
startedExecution = true;
|
||||
co_return;
|
||||
};
|
||||
|
||||
auto t = f();
|
||||
|
||||
cppcoro::shared_task<> sharedT = cppcoro::make_shared_task(std::move(t));
|
||||
|
||||
CHECK(!sharedT.is_ready());
|
||||
CHECK(!startedExecution);
|
||||
|
||||
auto consume = [](cppcoro::shared_task<> t) -> cppcoro::task<>
|
||||
{
|
||||
co_await t;
|
||||
};
|
||||
|
||||
auto c1 = consume(sharedT);
|
||||
cppcoro::sync_wait(c1);
|
||||
|
||||
CHECK(startedExecution);
|
||||
|
||||
auto c2 = consume(sharedT);
|
||||
cppcoro::sync_wait(c2);
|
||||
|
||||
CHECK(c1.is_ready());
|
||||
CHECK(c2.is_ready());
|
||||
}
|
||||
|
||||
TEST_CASE("shared_task<void> fmap operator")
|
||||
{
|
||||
cppcoro::single_consumer_event event;
|
||||
int value = 0;
|
||||
|
||||
auto setNumber = [&]() -> cppcoro::shared_task<>
|
||||
{
|
||||
co_await event;
|
||||
value = 123;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
auto numericStringTask =
|
||||
setNumber()
|
||||
| cppcoro::fmap([&]() { return std::to_string(value); });
|
||||
|
||||
CHECK(co_await numericStringTask == "123");
|
||||
}(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(value == 0);
|
||||
event.set();
|
||||
CHECK(value == 123);
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_CASE("shared_task<T> fmap operator")
|
||||
{
|
||||
cppcoro::single_consumer_event event;
|
||||
|
||||
auto getNumber = [&]() -> cppcoro::shared_task<int>
|
||||
{
|
||||
co_await event;
|
||||
co_return 123;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
auto numericStringTask =
|
||||
getNumber()
|
||||
| cppcoro::fmap([](int x) { return std::to_string(x); });
|
||||
|
||||
CHECK(co_await numericStringTask == "123");
|
||||
}(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
event.set();
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,93 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/single_consumer_async_auto_reset_event.hpp>
|
||||
|
||||
#include <cppcoro/config.hpp>
|
||||
#include <cppcoro/task.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
#include <cppcoro/when_all.hpp>
|
||||
#include <cppcoro/when_all_ready.hpp>
|
||||
#include <cppcoro/on_scope_exit.hpp>
|
||||
#include <cppcoro/static_thread_pool.hpp>
|
||||
|
||||
#include <thread>
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("single_consumer_async_auto_reset_event");
|
||||
|
||||
TEST_CASE("single waiter")
|
||||
{
|
||||
cppcoro::single_consumer_async_auto_reset_event event;
|
||||
|
||||
bool started = false;
|
||||
bool finished = false;
|
||||
auto run = [&]() -> cppcoro::task<>
|
||||
{
|
||||
started = true;
|
||||
co_await event;
|
||||
finished = true;
|
||||
};
|
||||
|
||||
auto check = [&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(started);
|
||||
CHECK(!finished);
|
||||
|
||||
event.set();
|
||||
|
||||
CHECK(finished);
|
||||
|
||||
co_return;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(run(), check()));
|
||||
}
|
||||
|
||||
TEST_CASE("multi-threaded")
|
||||
{
|
||||
cppcoro::static_thread_pool tp;
|
||||
|
||||
cppcoro::sync_wait([&]() -> cppcoro::task<>
|
||||
{
|
||||
cppcoro::single_consumer_async_auto_reset_event valueChangedEvent;
|
||||
|
||||
std::atomic<int> value;
|
||||
|
||||
auto consumer = [&]() -> cppcoro::task<int>
|
||||
{
|
||||
while (value.load(std::memory_order_relaxed) < 10'000)
|
||||
{
|
||||
co_await valueChangedEvent;
|
||||
}
|
||||
|
||||
co_return 0;
|
||||
};
|
||||
|
||||
auto modifier = [&](int count) -> cppcoro::task<int>
|
||||
{
|
||||
co_await tp.schedule();
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
value.fetch_add(1, std::memory_order_relaxed);
|
||||
valueChangedEvent.set();
|
||||
}
|
||||
co_return 0;
|
||||
};
|
||||
|
||||
for (int i = 0; i < 1000; ++i)
|
||||
{
|
||||
value.store(0, std::memory_order_relaxed);
|
||||
|
||||
// Really just checking that we don't deadlock here due to a missed wake-up.
|
||||
(void)co_await cppcoro::when_all(consumer(), modifier(5'000), modifier(5'000));
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,95 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/single_producer_sequencer.hpp>
|
||||
|
||||
#include <cppcoro/config.hpp>
|
||||
#include <cppcoro/sequence_barrier.hpp>
|
||||
#include <cppcoro/sequence_traits.hpp>
|
||||
#include <cppcoro/on_scope_exit.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
#include <cppcoro/when_all.hpp>
|
||||
#include <cppcoro/task.hpp>
|
||||
#include <cppcoro/static_thread_pool.hpp>
|
||||
|
||||
#include <thread>
|
||||
|
||||
#include <ostream>
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
DOCTEST_TEST_SUITE_BEGIN("single_producer_sequencer");
|
||||
|
||||
using namespace cppcoro;
|
||||
|
||||
DOCTEST_TEST_CASE("multi-threaded usage single consumer")
|
||||
{
|
||||
static_thread_pool tp{ 2 };
|
||||
|
||||
constexpr std::size_t bufferSize = 256;
|
||||
|
||||
sequence_barrier<std::size_t> readBarrier;
|
||||
single_producer_sequencer<std::size_t> sequencer(readBarrier, bufferSize);
|
||||
|
||||
constexpr std::size_t iterationCount = 1'000'000;
|
||||
|
||||
std::uint64_t buffer[bufferSize];
|
||||
|
||||
auto[result, dummy] = sync_wait(when_all(
|
||||
[&]() -> task<std::uint64_t>
|
||||
{
|
||||
// Consumer
|
||||
std::uint64_t sum = 0;
|
||||
|
||||
bool reachedEnd = false;
|
||||
std::size_t nextToRead = 0;
|
||||
do
|
||||
{
|
||||
const std::size_t available = co_await sequencer.wait_until_published(nextToRead, tp);
|
||||
do
|
||||
{
|
||||
sum += buffer[nextToRead % bufferSize];
|
||||
} while (nextToRead++ != available);
|
||||
|
||||
// Zero value is sentinel that indicates the end of the stream.
|
||||
reachedEnd = buffer[available % bufferSize] == 0;
|
||||
|
||||
// Notify that we've finished processing up to 'available'.
|
||||
readBarrier.publish(available);
|
||||
} while (!reachedEnd);
|
||||
|
||||
co_return sum;
|
||||
}(),
|
||||
[&]() -> task<>
|
||||
{
|
||||
// Producer
|
||||
constexpr std::size_t maxBatchSize = 10;
|
||||
|
||||
std::size_t i = 0;
|
||||
while (i < iterationCount)
|
||||
{
|
||||
const std::size_t batchSize = std::min(maxBatchSize, iterationCount - i);
|
||||
auto sequences = co_await sequencer.claim_up_to(batchSize, tp);
|
||||
for (auto seq : sequences)
|
||||
{
|
||||
buffer[seq % bufferSize] = ++i;
|
||||
}
|
||||
sequencer.publish(sequences.back());
|
||||
}
|
||||
|
||||
auto finalSeq = co_await sequencer.claim_one(tp);
|
||||
buffer[finalSeq % bufferSize] = 0;
|
||||
sequencer.publish(finalSeq);
|
||||
}()));
|
||||
|
||||
// Suppress unused variable warning.
|
||||
(void)dummy;
|
||||
|
||||
constexpr std::uint64_t expectedResult =
|
||||
std::uint64_t(iterationCount) * std::uint64_t(iterationCount + 1) / 2;
|
||||
|
||||
CHECK(result == expectedResult);
|
||||
}
|
||||
|
||||
DOCTEST_TEST_SUITE_END();
|
||||
@@ -0,0 +1,474 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/io_service.hpp>
|
||||
#include <cppcoro/net/socket.hpp>
|
||||
#include <cppcoro/task.hpp>
|
||||
#include <cppcoro/when_all.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
#include <cppcoro/on_scope_exit.hpp>
|
||||
#include <cppcoro/cancellation_source.hpp>
|
||||
#include <cppcoro/cancellation_token.hpp>
|
||||
#include <cppcoro/async_scope.hpp>
|
||||
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
using namespace cppcoro;
|
||||
using namespace cppcoro::net;
|
||||
|
||||
TEST_SUITE_BEGIN("socket");
|
||||
|
||||
TEST_CASE("create TCP/IPv4")
|
||||
{
|
||||
io_service ioSvc;
|
||||
auto socket = socket::create_tcpv4(ioSvc);
|
||||
}
|
||||
|
||||
TEST_CASE("create TCP/IPv6")
|
||||
{
|
||||
io_service ioSvc;
|
||||
auto socket = socket::create_tcpv6(ioSvc);
|
||||
}
|
||||
|
||||
TEST_CASE("create UDP/IPv4")
|
||||
{
|
||||
io_service ioSvc;
|
||||
auto socket = socket::create_udpv4(ioSvc);
|
||||
}
|
||||
|
||||
TEST_CASE("create UDP/IPv6")
|
||||
{
|
||||
io_service ioSvc;
|
||||
auto socket = socket::create_udpv6(ioSvc);
|
||||
}
|
||||
|
||||
TEST_CASE("TCP/IPv4 connect/disconnect")
|
||||
{
|
||||
io_service ioSvc;
|
||||
|
||||
ip_endpoint serverAddress;
|
||||
|
||||
task<int> serverTask;
|
||||
|
||||
auto server = [&](socket listeningSocket) -> task<int>
|
||||
{
|
||||
auto s = socket::create_tcpv4(ioSvc);
|
||||
co_await listeningSocket.accept(s);
|
||||
co_await s.disconnect();
|
||||
co_return 0;
|
||||
};
|
||||
|
||||
{
|
||||
auto serverSocket = socket::create_tcpv4(ioSvc);
|
||||
serverSocket.bind(ipv4_endpoint{ ipv4_address::loopback(), 0 });
|
||||
serverSocket.listen(3);
|
||||
serverAddress = serverSocket.local_endpoint();
|
||||
serverTask = server(std::move(serverSocket));
|
||||
}
|
||||
|
||||
auto client = [&]() -> task<int>
|
||||
{
|
||||
auto s = socket::create_tcpv4(ioSvc);
|
||||
s.bind(ipv4_endpoint{ ipv4_address::loopback(), 0 });
|
||||
co_await s.connect(serverAddress);
|
||||
co_await s.disconnect();
|
||||
co_return 0;
|
||||
};
|
||||
|
||||
task<int> clientTask = client();
|
||||
|
||||
(void)sync_wait(when_all(
|
||||
[&]() -> task<int>
|
||||
{
|
||||
auto stopOnExit = on_scope_exit([&] { ioSvc.stop(); });
|
||||
(void)co_await when_all(std::move(serverTask), std::move(clientTask));
|
||||
co_return 0;
|
||||
}(),
|
||||
[&]() -> task<int>
|
||||
{
|
||||
ioSvc.process_events();
|
||||
co_return 0;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_CASE("send/recv TCP/IPv4")
|
||||
{
|
||||
io_service ioSvc;
|
||||
|
||||
auto listeningSocket = socket::create_tcpv4(ioSvc);
|
||||
|
||||
listeningSocket.bind(ipv4_endpoint{ ipv4_address::loopback(), 0 });
|
||||
listeningSocket.listen(3);
|
||||
|
||||
auto echoServer = [&]() -> task<int>
|
||||
{
|
||||
auto acceptingSocket = socket::create_tcpv4(ioSvc);
|
||||
|
||||
co_await listeningSocket.accept(acceptingSocket);
|
||||
|
||||
std::uint8_t buffer[64];
|
||||
std::size_t bytesReceived;
|
||||
do
|
||||
{
|
||||
bytesReceived = co_await acceptingSocket.recv(buffer, sizeof(buffer));
|
||||
if (bytesReceived > 0)
|
||||
{
|
||||
std::size_t bytesSent = 0;
|
||||
do
|
||||
{
|
||||
bytesSent += co_await acceptingSocket.send(
|
||||
buffer + bytesSent,
|
||||
bytesReceived - bytesSent);
|
||||
} while (bytesSent < bytesReceived);
|
||||
}
|
||||
} while (bytesReceived > 0);
|
||||
|
||||
acceptingSocket.close_send();
|
||||
|
||||
co_await acceptingSocket.disconnect();
|
||||
|
||||
co_return 0;
|
||||
};
|
||||
|
||||
auto echoClient = [&]() -> task<int>
|
||||
{
|
||||
auto connectingSocket = socket::create_tcpv4(ioSvc);
|
||||
|
||||
connectingSocket.bind(ipv4_endpoint{});
|
||||
|
||||
co_await connectingSocket.connect(listeningSocket.local_endpoint());
|
||||
|
||||
auto receive = [&]() -> task<int>
|
||||
{
|
||||
std::uint8_t buffer[100];
|
||||
std::uint64_t totalBytesReceived = 0;
|
||||
std::size_t bytesReceived;
|
||||
do
|
||||
{
|
||||
bytesReceived = co_await connectingSocket.recv(buffer, sizeof(buffer));
|
||||
for (std::size_t i = 0; i < bytesReceived; ++i)
|
||||
{
|
||||
std::uint64_t byteIndex = totalBytesReceived + i;
|
||||
std::uint8_t expectedByte = 'a' + (byteIndex % 26);
|
||||
CHECK(buffer[i] == expectedByte);
|
||||
}
|
||||
|
||||
totalBytesReceived += bytesReceived;
|
||||
} while (bytesReceived > 0);
|
||||
|
||||
CHECK(totalBytesReceived == 1000);
|
||||
|
||||
co_return 0;
|
||||
};
|
||||
|
||||
auto send = [&]() -> task<int>
|
||||
{
|
||||
std::uint8_t buffer[100];
|
||||
for (std::uint64_t i = 0; i < 1000; i += sizeof(buffer))
|
||||
{
|
||||
for (std::size_t j = 0; j < sizeof(buffer); ++j)
|
||||
{
|
||||
buffer[j] = 'a' + ((i + j) % 26);
|
||||
}
|
||||
|
||||
std::size_t bytesSent = 0;
|
||||
do
|
||||
{
|
||||
bytesSent += co_await connectingSocket.send(buffer + bytesSent, sizeof(buffer) - bytesSent);
|
||||
} while (bytesSent < sizeof(buffer));
|
||||
}
|
||||
|
||||
connectingSocket.close_send();
|
||||
|
||||
co_return 0;
|
||||
};
|
||||
|
||||
co_await when_all(send(), receive());
|
||||
|
||||
co_await connectingSocket.disconnect();
|
||||
|
||||
co_return 0;
|
||||
};
|
||||
|
||||
(void)sync_wait(when_all(
|
||||
[&]() -> task<int>
|
||||
{
|
||||
auto stopOnExit = on_scope_exit([&] { ioSvc.stop(); });
|
||||
(void)co_await when_all(echoClient(), echoServer());
|
||||
co_return 0;
|
||||
}(),
|
||||
[&]() -> task<int>
|
||||
{
|
||||
ioSvc.process_events();
|
||||
co_return 0;
|
||||
}()));
|
||||
}
|
||||
|
||||
#if !CPPCORO_COMPILER_MSVC || CPPCORO_COMPILER_MSVC >= 192000000 || !CPPCORO_CPU_X86
|
||||
// HACK: Don't compile this function under MSVC x86.
|
||||
// It results in an ICE under VS 2017.15 and earlier.
|
||||
|
||||
TEST_CASE("send/recv TCP/IPv4 many connections")
|
||||
{
|
||||
io_service ioSvc;
|
||||
|
||||
auto listeningSocket = socket::create_tcpv4(ioSvc);
|
||||
|
||||
listeningSocket.bind(ipv4_endpoint{ ipv4_address::loopback(), 0 });
|
||||
listeningSocket.listen(20);
|
||||
|
||||
cancellation_source canceller;
|
||||
|
||||
auto handleConnection = [](socket s) -> task<void>
|
||||
{
|
||||
std::uint8_t buffer[64];
|
||||
std::size_t bytesReceived;
|
||||
do
|
||||
{
|
||||
bytesReceived = co_await s.recv(buffer, sizeof(buffer));
|
||||
if (bytesReceived > 0)
|
||||
{
|
||||
std::size_t bytesSent = 0;
|
||||
do
|
||||
{
|
||||
bytesSent += co_await s.send(
|
||||
buffer + bytesSent,
|
||||
bytesReceived - bytesSent);
|
||||
} while (bytesSent < bytesReceived);
|
||||
}
|
||||
} while (bytesReceived > 0);
|
||||
|
||||
s.close_send();
|
||||
|
||||
co_await s.disconnect();
|
||||
};
|
||||
|
||||
auto echoServer = [&](cancellation_token ct) -> task<>
|
||||
{
|
||||
async_scope connectionScope;
|
||||
|
||||
std::exception_ptr ex;
|
||||
try
|
||||
{
|
||||
while (true) {
|
||||
auto acceptingSocket = socket::create_tcpv4(ioSvc);
|
||||
co_await listeningSocket.accept(acceptingSocket, ct);
|
||||
connectionScope.spawn(
|
||||
handleConnection(std::move(acceptingSocket)));
|
||||
}
|
||||
}
|
||||
catch (const cppcoro::operation_cancelled&)
|
||||
{
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
ex = std::current_exception();
|
||||
}
|
||||
|
||||
co_await connectionScope.join();
|
||||
|
||||
if (ex)
|
||||
{
|
||||
std::rethrow_exception(ex);
|
||||
}
|
||||
};
|
||||
|
||||
auto echoClient = [&]() -> task<>
|
||||
{
|
||||
auto connectingSocket = socket::create_tcpv4(ioSvc);
|
||||
|
||||
connectingSocket.bind(ipv4_endpoint{});
|
||||
|
||||
co_await connectingSocket.connect(listeningSocket.local_endpoint());
|
||||
|
||||
auto receive = [&]() -> task<>
|
||||
{
|
||||
std::uint8_t buffer[100];
|
||||
std::uint64_t totalBytesReceived = 0;
|
||||
std::size_t bytesReceived;
|
||||
do
|
||||
{
|
||||
bytesReceived = co_await connectingSocket.recv(buffer, sizeof(buffer));
|
||||
for (std::size_t i = 0; i < bytesReceived; ++i)
|
||||
{
|
||||
std::uint64_t byteIndex = totalBytesReceived + i;
|
||||
std::uint8_t expectedByte = 'a' + (byteIndex % 26);
|
||||
CHECK(buffer[i] == expectedByte);
|
||||
}
|
||||
|
||||
totalBytesReceived += bytesReceived;
|
||||
} while (bytesReceived > 0);
|
||||
|
||||
CHECK(totalBytesReceived == 1000);
|
||||
};
|
||||
|
||||
auto send = [&]() -> task<>
|
||||
{
|
||||
std::uint8_t buffer[100];
|
||||
for (std::uint64_t i = 0; i < 1000; i += sizeof(buffer))
|
||||
{
|
||||
for (std::size_t j = 0; j < sizeof(buffer); ++j)
|
||||
{
|
||||
buffer[j] = 'a' + ((i + j) % 26);
|
||||
}
|
||||
|
||||
std::size_t bytesSent = 0;
|
||||
do
|
||||
{
|
||||
bytesSent += co_await connectingSocket.send(buffer + bytesSent, sizeof(buffer) - bytesSent);
|
||||
} while (bytesSent < sizeof(buffer));
|
||||
}
|
||||
|
||||
connectingSocket.close_send();
|
||||
};
|
||||
|
||||
co_await when_all(send(), receive());
|
||||
|
||||
co_await connectingSocket.disconnect();
|
||||
};
|
||||
|
||||
auto manyEchoClients = [&](int count) -> task<void>
|
||||
{
|
||||
auto shutdownServerOnExit = on_scope_exit([&]
|
||||
{
|
||||
canceller.request_cancellation();
|
||||
});
|
||||
|
||||
std::vector<task<>> clientTasks;
|
||||
clientTasks.reserve(count);
|
||||
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
clientTasks.emplace_back(echoClient());
|
||||
}
|
||||
|
||||
co_await when_all(std::move(clientTasks));
|
||||
};
|
||||
|
||||
(void)sync_wait(when_all(
|
||||
[&]() -> task<>
|
||||
{
|
||||
auto stopOnExit = on_scope_exit([&] { ioSvc.stop(); });
|
||||
(void)co_await when_all(
|
||||
manyEchoClients(20),
|
||||
echoServer(canceller.token()));
|
||||
}(),
|
||||
[&]() -> task<>
|
||||
{
|
||||
ioSvc.process_events();
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
TEST_CASE("udp send_to/recv_from")
|
||||
{
|
||||
io_service ioSvc;
|
||||
|
||||
auto server = [&](socket serverSocket) -> task<int>
|
||||
{
|
||||
std::uint8_t buffer[100];
|
||||
|
||||
auto[bytesReceived, remoteEndPoint] = co_await serverSocket.recv_from(buffer, 100);
|
||||
CHECK(bytesReceived == 50);
|
||||
|
||||
// Send an ACK response.
|
||||
{
|
||||
const std::uint8_t response[1] = { 0 };
|
||||
co_await serverSocket.send_to(remoteEndPoint, &response, 1);
|
||||
}
|
||||
|
||||
// Second message received won't fit within buffer.
|
||||
try
|
||||
{
|
||||
std::tie(bytesReceived, remoteEndPoint) = co_await serverSocket.recv_from(buffer, 100);
|
||||
FAIL("Should have thrown");
|
||||
}
|
||||
catch (const std::system_error&)
|
||||
{
|
||||
// TODO: Map this situation to some kind of error_condition value.
|
||||
// The win32 ERROR_MORE_DATA error code doesn't seem to map to any of the standard std::errc values.
|
||||
//
|
||||
// CHECK(ex.code() == ???);
|
||||
//
|
||||
// Possibly also need to switch to returning a std::error_code directly rather than
|
||||
// throwing a std::system_error for this case.
|
||||
}
|
||||
|
||||
// Send an NACK response.
|
||||
{
|
||||
const std::uint8_t response[1] = { 1 };
|
||||
co_await serverSocket.send_to(remoteEndPoint, response, 1);
|
||||
}
|
||||
|
||||
co_return 0;
|
||||
};
|
||||
|
||||
ip_endpoint serverAddress;
|
||||
|
||||
task<int> serverTask;
|
||||
|
||||
{
|
||||
auto serverSocket = socket::create_udpv4(ioSvc);
|
||||
serverSocket.bind(ipv4_endpoint{ ipv4_address::loopback(), 0 });
|
||||
serverAddress = serverSocket.local_endpoint();
|
||||
serverTask = server(std::move(serverSocket));
|
||||
}
|
||||
|
||||
auto client = [&]() -> task<int>
|
||||
{
|
||||
auto socket = socket::create_udpv4(ioSvc);
|
||||
|
||||
// don't need to bind(), should be implicitly bound on first send_to().
|
||||
|
||||
// Send first message of 50 bytes
|
||||
{
|
||||
std::uint8_t buffer[50] = { 0 };
|
||||
co_await socket.send_to(serverAddress, buffer, 50);
|
||||
}
|
||||
|
||||
// Receive ACK message
|
||||
{
|
||||
std::uint8_t buffer[1];
|
||||
auto[bytesReceived, ackAddress] = co_await socket.recv_from(buffer, 1);
|
||||
CHECK(bytesReceived == 1);
|
||||
CHECK(buffer[0] == 0);
|
||||
CHECK(ackAddress == serverAddress);
|
||||
}
|
||||
|
||||
// Send second message of 128 bytes
|
||||
{
|
||||
std::uint8_t buffer[128] = { 0 };
|
||||
co_await socket.send_to(serverAddress, buffer, 128);
|
||||
}
|
||||
|
||||
// Receive NACK message
|
||||
{
|
||||
std::uint8_t buffer[1];
|
||||
auto[bytesReceived, ackAddress] = co_await socket.recv_from(buffer, 1);
|
||||
CHECK(bytesReceived == 1);
|
||||
CHECK(buffer[0] == 1);
|
||||
CHECK(ackAddress == serverAddress);
|
||||
}
|
||||
|
||||
co_return 0;
|
||||
};
|
||||
|
||||
(void)sync_wait(when_all(
|
||||
[&]() -> task<int>
|
||||
{
|
||||
auto stopOnExit = on_scope_exit([&] { ioSvc.stop(); });
|
||||
(void)co_await when_all(std::move(serverTask), client());
|
||||
co_return 0;
|
||||
}(),
|
||||
[&]() -> task<int>
|
||||
{
|
||||
ioSvc.process_events();
|
||||
co_return 0;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,290 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/static_thread_pool.hpp>
|
||||
#include <cppcoro/task.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
#include <cppcoro/when_all.hpp>
|
||||
|
||||
#include <vector>
|
||||
#include <thread>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <numeric>
|
||||
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("static_thread_pool");
|
||||
|
||||
TEST_CASE("construct/destruct")
|
||||
{
|
||||
cppcoro::static_thread_pool threadPool;
|
||||
CHECK(threadPool.thread_count() == std::thread::hardware_concurrency());
|
||||
}
|
||||
|
||||
TEST_CASE("construct/destruct to specific thread count")
|
||||
{
|
||||
cppcoro::static_thread_pool threadPool{ 5 };
|
||||
CHECK(threadPool.thread_count() == 5);
|
||||
}
|
||||
|
||||
TEST_CASE("run one task")
|
||||
{
|
||||
cppcoro::static_thread_pool threadPool{ 2 };
|
||||
|
||||
auto initiatingThreadId = std::this_thread::get_id();
|
||||
|
||||
cppcoro::sync_wait([&]() -> cppcoro::task<void>
|
||||
{
|
||||
co_await threadPool.schedule();
|
||||
if (std::this_thread::get_id() == initiatingThreadId)
|
||||
{
|
||||
FAIL("schedule() did not switch threads");
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_CASE("launch many tasks remotely")
|
||||
{
|
||||
cppcoro::static_thread_pool threadPool;
|
||||
|
||||
auto makeTask = [&]() -> cppcoro::task<>
|
||||
{
|
||||
co_await threadPool.schedule();
|
||||
};
|
||||
|
||||
std::vector<cppcoro::task<>> tasks;
|
||||
for (std::uint32_t i = 0; i < 100; ++i)
|
||||
{
|
||||
tasks.push_back(makeTask());
|
||||
}
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all(std::move(tasks)));
|
||||
}
|
||||
|
||||
cppcoro::task<std::uint64_t> sum_of_squares(
|
||||
std::uint32_t start,
|
||||
std::uint32_t end,
|
||||
cppcoro::static_thread_pool& tp)
|
||||
{
|
||||
co_await tp.schedule();
|
||||
|
||||
auto count = end - start;
|
||||
if (count > 1000)
|
||||
{
|
||||
auto half = start + count / 2;
|
||||
auto[a, b] = co_await cppcoro::when_all(
|
||||
sum_of_squares(start, half, tp),
|
||||
sum_of_squares(half, end, tp));
|
||||
co_return a + b;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::uint64_t sum = 0;
|
||||
for (std::uint64_t x = start; x < end; ++x)
|
||||
{
|
||||
sum += x * x;
|
||||
}
|
||||
co_return sum;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("launch sub-task with many sub-tasks")
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
constexpr std::uint64_t limit = 1'000'000'000;
|
||||
|
||||
cppcoro::static_thread_pool tp;
|
||||
|
||||
// Wait for the thread-pool thread to start up.
|
||||
std::this_thread::sleep_for(1ms);
|
||||
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
auto result = cppcoro::sync_wait(sum_of_squares(0, limit , tp));
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
|
||||
std::uint64_t sum = 0;
|
||||
for (std::uint64_t i = 0; i < limit; ++i)
|
||||
{
|
||||
sum += i * i;
|
||||
}
|
||||
|
||||
auto end2 = std::chrono::high_resolution_clock::now();
|
||||
|
||||
auto toNs = [](auto time)
|
||||
{
|
||||
return std::chrono::duration_cast<std::chrono::nanoseconds>(time).count();
|
||||
};
|
||||
|
||||
std::cout
|
||||
<< "multi-threaded version took " << toNs(end - start) << "ns\n"
|
||||
<< "single-threaded version took " << toNs(end2 - end) << "ns" << std::endl;
|
||||
|
||||
CHECK(result == sum);
|
||||
}
|
||||
|
||||
struct fork_join_operation
|
||||
{
|
||||
std::atomic<std::size_t> m_count;
|
||||
cppcoro::coroutine_handle<> m_coro;
|
||||
|
||||
fork_join_operation() : m_count(1) {}
|
||||
|
||||
void begin_work() noexcept
|
||||
{
|
||||
m_count.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void end_work() noexcept
|
||||
{
|
||||
if (m_count.fetch_sub(1, std::memory_order_acq_rel) == 1)
|
||||
{
|
||||
m_coro.resume();
|
||||
}
|
||||
}
|
||||
|
||||
bool await_ready() noexcept { return m_count.load(std::memory_order_acquire) == 1; }
|
||||
|
||||
bool await_suspend(cppcoro::coroutine_handle<> coro) noexcept
|
||||
{
|
||||
m_coro = coro;
|
||||
return m_count.fetch_sub(1, std::memory_order_acq_rel) != 1;
|
||||
}
|
||||
|
||||
void await_resume() noexcept {};
|
||||
};
|
||||
|
||||
template<typename FUNC, typename RANGE, typename SCHEDULER>
|
||||
cppcoro::task<void> for_each_async(SCHEDULER& scheduler, RANGE& range, FUNC func)
|
||||
{
|
||||
using reference_type = decltype(*range.begin());
|
||||
|
||||
// TODO: Use awaiter_t here instead. This currently assumes that
|
||||
// result of scheduler.schedule() doesn't have an operator co_await().
|
||||
using schedule_operation = decltype(scheduler.schedule());
|
||||
|
||||
struct work_operation
|
||||
{
|
||||
fork_join_operation& m_forkJoin;
|
||||
FUNC& m_func;
|
||||
reference_type m_value;
|
||||
schedule_operation m_scheduleOp;
|
||||
|
||||
work_operation(fork_join_operation& forkJoin, SCHEDULER& scheduler, FUNC& func, reference_type&& value)
|
||||
: m_forkJoin(forkJoin)
|
||||
, m_func(func)
|
||||
, m_value(static_cast<reference_type&&>(value))
|
||||
, m_scheduleOp(scheduler.schedule())
|
||||
{
|
||||
}
|
||||
|
||||
bool await_ready() noexcept { return false; }
|
||||
|
||||
CPPCORO_NOINLINE
|
||||
void await_suspend(cppcoro::coroutine_handle<> coro) noexcept
|
||||
{
|
||||
fork_join_operation& forkJoin = m_forkJoin;
|
||||
FUNC& func = m_func;
|
||||
reference_type value = static_cast<reference_type&&>(m_value);
|
||||
|
||||
static_assert(std::is_same_v<decltype(m_scheduleOp.await_suspend(coro)), void>);
|
||||
|
||||
forkJoin.begin_work();
|
||||
|
||||
// Schedule the next iteration of the loop to run
|
||||
m_scheduleOp.await_suspend(coro);
|
||||
|
||||
func(static_cast<reference_type&&>(value));
|
||||
|
||||
forkJoin.end_work();
|
||||
}
|
||||
|
||||
void await_resume() noexcept {}
|
||||
};
|
||||
|
||||
co_await scheduler.schedule();
|
||||
|
||||
fork_join_operation forkJoin;
|
||||
|
||||
for (auto&& x : range)
|
||||
{
|
||||
co_await work_operation{
|
||||
forkJoin,
|
||||
scheduler,
|
||||
func,
|
||||
static_cast<decltype(x)>(x)
|
||||
};
|
||||
}
|
||||
|
||||
co_await forkJoin;
|
||||
}
|
||||
|
||||
std::uint64_t collatz_distance(std::uint64_t number)
|
||||
{
|
||||
std::uint64_t count = 0;
|
||||
while (number > 1)
|
||||
{
|
||||
if (number % 2 == 0) number /= 2;
|
||||
else number = number * 3 + 1;
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
TEST_CASE("for_each_async")
|
||||
{
|
||||
cppcoro::static_thread_pool tp;
|
||||
|
||||
{
|
||||
std::vector<std::uint64_t> values(1'000'000);
|
||||
std::iota(values.begin(), values.end(), 1);
|
||||
|
||||
cppcoro::sync_wait([&]() -> cppcoro::task<>
|
||||
{
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
co_await for_each_async(tp, values, [](std::uint64_t& value)
|
||||
{
|
||||
value = collatz_distance(value);
|
||||
});
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
|
||||
std::cout << "for_each_async of " << values.size()
|
||||
<< " took " << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count()
|
||||
<< "us" << std::endl;
|
||||
|
||||
for (std::size_t i = 0; i < 1'000'000; ++i)
|
||||
{
|
||||
CHECK(values[i] == collatz_distance(i + 1));
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
{
|
||||
std::vector<std::uint64_t> values(1'000'000);
|
||||
std::iota(values.begin(), values.end(), 1);
|
||||
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
for (auto&& x : values)
|
||||
{
|
||||
x = collatz_distance(x);
|
||||
}
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
|
||||
std::cout << "single-threaded for loop of " << values.size()
|
||||
<< " took " << std::chrono::duration_cast<std::chrono::microseconds>(end - start).count()
|
||||
<< "us" << std::endl;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,76 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
|
||||
#include <cppcoro/config.hpp>
|
||||
#include <cppcoro/task.hpp>
|
||||
#include <cppcoro/shared_task.hpp>
|
||||
#include <cppcoro/on_scope_exit.hpp>
|
||||
#include <cppcoro/static_thread_pool.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("sync_wait");
|
||||
|
||||
static_assert(std::is_same<
|
||||
decltype(cppcoro::sync_wait(std::declval<cppcoro::task<std::string>>())),
|
||||
std::string&&>::value);
|
||||
static_assert(std::is_same<
|
||||
decltype(cppcoro::sync_wait(std::declval<cppcoro::task<std::string>&>())),
|
||||
std::string&>::value);
|
||||
|
||||
TEST_CASE("sync_wait(task<T>)")
|
||||
{
|
||||
auto makeTask = []() -> cppcoro::task<std::string>
|
||||
{
|
||||
co_return "foo";
|
||||
};
|
||||
|
||||
auto task = makeTask();
|
||||
CHECK(cppcoro::sync_wait(task) == "foo");
|
||||
|
||||
CHECK(cppcoro::sync_wait(makeTask()) == "foo");
|
||||
}
|
||||
|
||||
TEST_CASE("sync_wait(shared_task<T>)")
|
||||
{
|
||||
auto makeTask = []() -> cppcoro::shared_task<std::string>
|
||||
{
|
||||
co_return "foo";
|
||||
};
|
||||
|
||||
auto task = makeTask();
|
||||
|
||||
CHECK(cppcoro::sync_wait(task) == "foo");
|
||||
CHECK(cppcoro::sync_wait(makeTask()) == "foo");
|
||||
}
|
||||
|
||||
TEST_CASE("multiple threads")
|
||||
{
|
||||
// We are creating a new task and starting it inside the sync_wait().
|
||||
// The task will reschedule itself for resumption on a thread-pool thread
|
||||
// which will sometimes complete before this thread calls event.wait()
|
||||
// inside sync_wait(). Thus we're roughly testing the thread-safety of
|
||||
// sync_wait().
|
||||
cppcoro::static_thread_pool tp{ 1 };
|
||||
|
||||
int value = 0;
|
||||
auto createLazyTask = [&]() -> cppcoro::task<int>
|
||||
{
|
||||
co_await tp.schedule();
|
||||
co_return value++;
|
||||
};
|
||||
|
||||
for (int i = 0; i < 10'000; ++i)
|
||||
{
|
||||
CHECK(cppcoro::sync_wait(createLazyTask()) == i);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,349 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/task.hpp>
|
||||
#include <cppcoro/single_consumer_event.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
#include <cppcoro/when_all_ready.hpp>
|
||||
#include <cppcoro/fmap.hpp>
|
||||
|
||||
#include "counted.hpp"
|
||||
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("task");
|
||||
|
||||
TEST_CASE("task doesn't start until awaited")
|
||||
{
|
||||
bool started = false;
|
||||
auto func = [&]() -> cppcoro::task<>
|
||||
{
|
||||
started = true;
|
||||
co_return;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait([&]() -> cppcoro::task<>
|
||||
{
|
||||
auto t = func();
|
||||
CHECK(!started);
|
||||
|
||||
co_await t;
|
||||
|
||||
CHECK(started);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_CASE("awaiting default-constructed task throws broken_promise")
|
||||
{
|
||||
cppcoro::sync_wait([&]() -> cppcoro::task<>
|
||||
{
|
||||
cppcoro::task<> t;
|
||||
CHECK_THROWS_AS(co_await t, const cppcoro::broken_promise&);
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_CASE("awaiting task that completes asynchronously")
|
||||
{
|
||||
bool reachedBeforeEvent = false;
|
||||
bool reachedAfterEvent = false;
|
||||
cppcoro::single_consumer_event event;
|
||||
auto f = [&]() -> cppcoro::task<>
|
||||
{
|
||||
reachedBeforeEvent = true;
|
||||
co_await event;
|
||||
reachedAfterEvent = true;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait([&]() -> cppcoro::task<>
|
||||
{
|
||||
auto t = f();
|
||||
|
||||
CHECK(!reachedBeforeEvent);
|
||||
|
||||
(void)co_await cppcoro::when_all_ready(
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
co_await t;
|
||||
CHECK(reachedBeforeEvent);
|
||||
CHECK(reachedAfterEvent);
|
||||
}(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(reachedBeforeEvent);
|
||||
CHECK(!reachedAfterEvent);
|
||||
event.set();
|
||||
CHECK(reachedAfterEvent);
|
||||
co_return;
|
||||
}());
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_CASE("destroying task that was never awaited destroys captured args")
|
||||
{
|
||||
counted::reset_counts();
|
||||
|
||||
auto f = [](counted c) -> cppcoro::task<counted>
|
||||
{
|
||||
co_return c;
|
||||
};
|
||||
|
||||
CHECK(counted::active_count() == 0);
|
||||
|
||||
{
|
||||
auto t = f(counted{});
|
||||
CHECK(counted::active_count() == 1);
|
||||
}
|
||||
|
||||
CHECK(counted::active_count() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("task destructor destroys result")
|
||||
{
|
||||
counted::reset_counts();
|
||||
|
||||
auto f = []() -> cppcoro::task<counted>
|
||||
{
|
||||
co_return counted{};
|
||||
};
|
||||
|
||||
{
|
||||
auto t = f();
|
||||
CHECK(counted::active_count() == 0);
|
||||
|
||||
auto& result = cppcoro::sync_wait(t);
|
||||
|
||||
CHECK(counted::active_count() == 1);
|
||||
CHECK(result.id == 0);
|
||||
}
|
||||
|
||||
CHECK(counted::active_count() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("task of reference type")
|
||||
{
|
||||
int value = 3;
|
||||
auto f = [&]() -> cppcoro::task<int&>
|
||||
{
|
||||
co_return value;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait([&]() -> cppcoro::task<>
|
||||
{
|
||||
SUBCASE("awaiting rvalue task")
|
||||
{
|
||||
decltype(auto) result = co_await f();
|
||||
static_assert(
|
||||
std::is_same<decltype(result), int&>::value,
|
||||
"co_await r-value reference of task<int&> should result in an int&");
|
||||
CHECK(&result == &value);
|
||||
}
|
||||
|
||||
SUBCASE("awaiting lvalue task")
|
||||
{
|
||||
auto t = f();
|
||||
decltype(auto) result = co_await t;
|
||||
static_assert(
|
||||
std::is_same<decltype(result), int&>::value,
|
||||
"co_await l-value reference of task<int&> should result in an int&");
|
||||
CHECK(&result == &value);
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_CASE("passing parameter by value to task coroutine calls move-constructor exactly once")
|
||||
{
|
||||
counted::reset_counts();
|
||||
|
||||
auto f = [](counted arg) -> cppcoro::task<>
|
||||
{
|
||||
co_return;
|
||||
};
|
||||
|
||||
counted c;
|
||||
|
||||
CHECK(counted::active_count() == 1);
|
||||
CHECK(counted::default_construction_count == 1);
|
||||
CHECK(counted::copy_construction_count == 0);
|
||||
CHECK(counted::move_construction_count == 0);
|
||||
CHECK(counted::destruction_count == 0);
|
||||
|
||||
{
|
||||
auto t = f(c);
|
||||
|
||||
// Should have called copy-constructor to pass a copy of 'c' into f by value.
|
||||
CHECK(counted::copy_construction_count == 1);
|
||||
|
||||
// Inside f it should have move-constructed parameter into coroutine frame variable
|
||||
//WARN_MESSAGE(counted::move_construction_count == 1,
|
||||
// "Known bug in MSVC 2017.1, not critical if it performs multiple moves");
|
||||
|
||||
// Active counts should be the instance 'c' and the instance captured in coroutine frame of 't'.
|
||||
CHECK(counted::active_count() == 2);
|
||||
}
|
||||
|
||||
CHECK(counted::active_count() == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("task<void> fmap pipe operator")
|
||||
{
|
||||
using cppcoro::fmap;
|
||||
|
||||
cppcoro::single_consumer_event event;
|
||||
|
||||
auto f = [&]() -> cppcoro::task<>
|
||||
{
|
||||
co_await event;
|
||||
co_return;
|
||||
};
|
||||
|
||||
auto t = f() | fmap([] { return 123; });
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(co_await t == 123);
|
||||
}(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
event.set();
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_CASE("task<int> fmap pipe operator")
|
||||
{
|
||||
using cppcoro::task;
|
||||
using cppcoro::fmap;
|
||||
using cppcoro::sync_wait;
|
||||
using cppcoro::make_task;
|
||||
|
||||
auto one = [&]() -> task<int>
|
||||
{
|
||||
co_return 1;
|
||||
};
|
||||
|
||||
SUBCASE("r-value fmap / r-value lambda")
|
||||
{
|
||||
auto t = one()
|
||||
| fmap([delta = 1](auto i) { return i + delta; });
|
||||
CHECK(sync_wait(t) == 2);
|
||||
}
|
||||
|
||||
SUBCASE("r-value fmap / l-value lambda")
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
|
||||
auto t = [&]
|
||||
{
|
||||
auto f = [prefix = "pfx"s](int x)
|
||||
{
|
||||
return prefix + std::to_string(x);
|
||||
};
|
||||
|
||||
// Want to make sure that the resulting awaitable has taken
|
||||
// a copy of the lambda passed to fmap().
|
||||
return one() | fmap(f);
|
||||
}();
|
||||
|
||||
CHECK(sync_wait(t) == "pfx1");
|
||||
}
|
||||
|
||||
SUBCASE("l-value fmap / r-value lambda")
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
|
||||
auto t = [&]
|
||||
{
|
||||
auto addprefix = fmap([prefix = "a really really long prefix that prevents small string optimisation"s](int x)
|
||||
{
|
||||
return prefix + std::to_string(x);
|
||||
});
|
||||
|
||||
// Want to make sure that the resulting awaitable has taken
|
||||
// a copy of the lambda passed to fmap().
|
||||
return one() | addprefix;
|
||||
}();
|
||||
|
||||
CHECK(sync_wait(t) == "a really really long prefix that prevents small string optimisation1");
|
||||
}
|
||||
|
||||
SUBCASE("l-value fmap / l-value lambda")
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
|
||||
task<std::string> t;
|
||||
|
||||
{
|
||||
auto lambda = [prefix = "a really really long prefix that prevents small string optimisation"s](int x)
|
||||
{
|
||||
return prefix + std::to_string(x);
|
||||
};
|
||||
|
||||
auto addprefix = fmap(lambda);
|
||||
|
||||
// Want to make sure that the resulting task has taken
|
||||
// a copy of the lambda passed to fmap().
|
||||
t = make_task(one() | addprefix);
|
||||
}
|
||||
|
||||
CHECK(!t.is_ready());
|
||||
|
||||
CHECK(sync_wait(t) == "a really really long prefix that prevents small string optimisation1");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("chained fmap pipe operations")
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
using cppcoro::task;
|
||||
using cppcoro::sync_wait;
|
||||
|
||||
auto prepend = [](std::string s)
|
||||
{
|
||||
using cppcoro::fmap;
|
||||
return fmap([s = std::move(s)](const std::string& value) { return s + value; });
|
||||
};
|
||||
|
||||
auto append = [](std::string s)
|
||||
{
|
||||
using cppcoro::fmap;
|
||||
return fmap([s = std::move(s)](const std::string& value){ return value + s; });
|
||||
};
|
||||
|
||||
auto asyncString = [](std::string s) -> task<std::string>
|
||||
{
|
||||
co_return std::move(s);
|
||||
};
|
||||
|
||||
auto t = asyncString("base"s) | prepend("pre_"s) | append("_post"s);
|
||||
|
||||
CHECK(sync_wait(t) == "pre_base_post");
|
||||
}
|
||||
|
||||
TEST_CASE("lots of synchronous completions doesn't result in stack-overflow")
|
||||
{
|
||||
auto completesSynchronously = []() -> cppcoro::task<int>
|
||||
{
|
||||
co_return 1;
|
||||
};
|
||||
|
||||
auto run = [&]() -> cppcoro::task<>
|
||||
{
|
||||
int sum = 0;
|
||||
for (int i = 0; i < 1'000'000; ++i)
|
||||
{
|
||||
sum += co_await completesSynchronously();
|
||||
}
|
||||
CHECK(sum == 1'000'000);
|
||||
};
|
||||
|
||||
cppcoro::sync_wait(run());
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,265 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/when_all.hpp>
|
||||
|
||||
#include <cppcoro/task.hpp>
|
||||
#include <cppcoro/shared_task.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
#include <cppcoro/async_manual_reset_event.hpp>
|
||||
|
||||
#include "counted.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <ostream>
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("when_all_ready");
|
||||
|
||||
template<template<typename T> class TASK, typename T>
|
||||
TASK<T> when_event_set_return(cppcoro::async_manual_reset_event& event, T value)
|
||||
{
|
||||
co_await event;
|
||||
co_return std::move(value);
|
||||
}
|
||||
|
||||
TEST_CASE("when_all_ready() with no args")
|
||||
{
|
||||
[[maybe_unused]] std::tuple<> result = cppcoro::sync_wait(cppcoro::when_all_ready());
|
||||
}
|
||||
|
||||
TEST_CASE("when_all_ready() with one task")
|
||||
{
|
||||
bool started = false;
|
||||
auto f = [&](cppcoro::async_manual_reset_event& event) -> cppcoro::task<>
|
||||
{
|
||||
started = true;
|
||||
co_await event;
|
||||
};
|
||||
|
||||
cppcoro::async_manual_reset_event event;
|
||||
auto whenAllAwaitable = cppcoro::when_all_ready(f(event));
|
||||
CHECK(!started);
|
||||
|
||||
bool finished = false;
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
auto&[t] = co_await whenAllAwaitable;
|
||||
finished = true;
|
||||
t.result();
|
||||
}(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(started);
|
||||
CHECK(!finished);
|
||||
event.set();
|
||||
CHECK(finished);
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_CASE("when_all_ready() with multiple task")
|
||||
{
|
||||
auto makeTask = [&](bool& started, cppcoro::async_manual_reset_event& event, int result) -> cppcoro::task<int>
|
||||
{
|
||||
started = true;
|
||||
co_await event;
|
||||
co_return result;
|
||||
};
|
||||
|
||||
cppcoro::async_manual_reset_event event1;
|
||||
cppcoro::async_manual_reset_event event2;
|
||||
bool started1 = false;
|
||||
bool started2 = false;
|
||||
auto whenAllAwaitable = cppcoro::when_all_ready(
|
||||
makeTask(started1, event1, 1),
|
||||
makeTask(started2, event2, 2));
|
||||
CHECK(!started1);
|
||||
CHECK(!started2);
|
||||
|
||||
bool whenAllAwaitableFinished = false;
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
auto[t1, t2] = co_await std::move(whenAllAwaitable);
|
||||
whenAllAwaitableFinished = true;
|
||||
CHECK(t1.result() == 1);
|
||||
CHECK(t2.result() == 2);
|
||||
}(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(started1);
|
||||
CHECK(started2);
|
||||
|
||||
event2.set();
|
||||
|
||||
CHECK(!whenAllAwaitableFinished);
|
||||
|
||||
event1.set();
|
||||
|
||||
CHECK(whenAllAwaitableFinished);
|
||||
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_CASE("when_all_ready() with all task types")
|
||||
{
|
||||
cppcoro::async_manual_reset_event event;
|
||||
auto t0 = when_event_set_return<cppcoro::task>(event, 1);
|
||||
auto t1 = when_event_set_return<cppcoro::shared_task>(event, 2);
|
||||
|
||||
auto allTask = cppcoro::when_all_ready(std::move(t0), t1);
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
auto [r0, r1] = co_await std::move(allTask);
|
||||
|
||||
CHECK(r0.result() == 1);
|
||||
CHECK(r1.result() == 2);
|
||||
}(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
event.set();
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_CASE("when_all_ready() with std::vector<task<T>>")
|
||||
{
|
||||
cppcoro::async_manual_reset_event event;
|
||||
|
||||
std::uint32_t startedCount = 0;
|
||||
std::uint32_t finishedCount = 0;
|
||||
|
||||
auto makeTask = [&]() -> cppcoro::task<>
|
||||
{
|
||||
++startedCount;
|
||||
co_await event;
|
||||
++finishedCount;
|
||||
};
|
||||
|
||||
std::vector<cppcoro::task<>> tasks;
|
||||
for (std::uint32_t i = 0; i < 10; ++i)
|
||||
{
|
||||
tasks.emplace_back(makeTask());
|
||||
}
|
||||
|
||||
auto allTask = cppcoro::when_all_ready(std::move(tasks));
|
||||
|
||||
// Shouldn't have started any tasks yet.
|
||||
CHECK(startedCount == 0u);
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
auto resultTasks = co_await std::move(allTask);
|
||||
CHECK(resultTasks.size() == 10u);
|
||||
|
||||
for (auto& t : resultTasks)
|
||||
{
|
||||
CHECK_NOTHROW(t.result());
|
||||
}
|
||||
}(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(startedCount == 10u);
|
||||
CHECK(finishedCount == 0u);
|
||||
|
||||
event.set();
|
||||
|
||||
CHECK(finishedCount == 10u);
|
||||
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_CASE("when_all_ready() with std::vector<shared_task<T>>")
|
||||
{
|
||||
cppcoro::async_manual_reset_event event;
|
||||
|
||||
std::uint32_t startedCount = 0;
|
||||
std::uint32_t finishedCount = 0;
|
||||
|
||||
auto makeTask = [&]() -> cppcoro::shared_task<>
|
||||
{
|
||||
++startedCount;
|
||||
co_await event;
|
||||
++finishedCount;
|
||||
};
|
||||
|
||||
std::vector<cppcoro::shared_task<>> tasks;
|
||||
for (std::uint32_t i = 0; i < 10; ++i)
|
||||
{
|
||||
tasks.emplace_back(makeTask());
|
||||
}
|
||||
|
||||
auto allTask = cppcoro::when_all_ready(std::move(tasks));
|
||||
|
||||
// Shouldn't have started any tasks yet.
|
||||
CHECK(startedCount == 0u);
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
auto resultTasks = co_await std::move(allTask);
|
||||
CHECK(resultTasks.size() == 10u);
|
||||
|
||||
for (auto& t : resultTasks)
|
||||
{
|
||||
CHECK_NOTHROW(t.result());
|
||||
}
|
||||
}(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(startedCount == 10u);
|
||||
CHECK(finishedCount == 0u);
|
||||
|
||||
event.set();
|
||||
|
||||
CHECK(finishedCount == 10u);
|
||||
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_CASE("when_all_ready() doesn't rethrow exceptions")
|
||||
{
|
||||
auto makeTask = [](bool throwException) -> cppcoro::task<int>
|
||||
{
|
||||
if (throwException)
|
||||
{
|
||||
throw std::exception{};
|
||||
}
|
||||
else
|
||||
{
|
||||
co_return 123;
|
||||
}
|
||||
};
|
||||
|
||||
cppcoro::sync_wait([&]() -> cppcoro::task<>
|
||||
{
|
||||
try
|
||||
{
|
||||
auto[t0, t1] = co_await cppcoro::when_all_ready(makeTask(true), makeTask(false));
|
||||
|
||||
// You can obtain the exceptions by re-awaiting the returned tasks.
|
||||
CHECK_THROWS_AS(t0.result(), const std::exception&);
|
||||
CHECK(t1.result() == 123);
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
FAIL("Shouldn't throw");
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
@@ -0,0 +1,427 @@
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Copyright (c) Lewis Baker
|
||||
// Licenced under MIT license. See LICENSE.txt for details.
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cppcoro/when_all.hpp>
|
||||
|
||||
#include <cppcoro/config.hpp>
|
||||
#include <cppcoro/async_manual_reset_event.hpp>
|
||||
#include <cppcoro/async_mutex.hpp>
|
||||
#include <cppcoro/fmap.hpp>
|
||||
#include <cppcoro/shared_task.hpp>
|
||||
#include <cppcoro/sync_wait.hpp>
|
||||
#include <cppcoro/task.hpp>
|
||||
|
||||
#include "counted.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <ostream>
|
||||
#include "doctest/cppcoro_doctest.h"
|
||||
|
||||
TEST_SUITE_BEGIN("when_all");
|
||||
|
||||
namespace
|
||||
{
|
||||
template<template<typename T> class TASK, typename T>
|
||||
TASK<T> when_event_set_return(cppcoro::async_manual_reset_event& event, T value)
|
||||
{
|
||||
co_await event;
|
||||
co_return std::move(value);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("when_all() with no args completes immediately")
|
||||
{
|
||||
[[maybe_unused]] std::tuple<> result = cppcoro::sync_wait(cppcoro::when_all());
|
||||
}
|
||||
|
||||
TEST_CASE("when_all() with one arg")
|
||||
{
|
||||
bool started = false;
|
||||
bool finished = false;
|
||||
auto f = [&](cppcoro::async_manual_reset_event& event) -> cppcoro::task<std::string>
|
||||
{
|
||||
started = true;
|
||||
co_await event;
|
||||
finished = true;
|
||||
co_return "foo";
|
||||
};
|
||||
|
||||
cppcoro::async_manual_reset_event event;
|
||||
|
||||
auto whenAllTask = cppcoro::when_all(f(event));
|
||||
CHECK(!started);
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
auto[s] = co_await whenAllTask;
|
||||
CHECK(s == "foo");
|
||||
}(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(started);
|
||||
CHECK(!finished);
|
||||
event.set();
|
||||
CHECK(finished);
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_CASE("when_all() with awaitables")
|
||||
{
|
||||
cppcoro::sync_wait([]() -> cppcoro::task<>
|
||||
{
|
||||
auto makeTask = [](int x) -> cppcoro::task<int>
|
||||
{
|
||||
co_return x;
|
||||
};
|
||||
|
||||
cppcoro::async_manual_reset_event event;
|
||||
event.set();
|
||||
|
||||
cppcoro::async_mutex mutex;
|
||||
|
||||
auto[eventResult, mutexLock, number] = co_await cppcoro::when_all(
|
||||
std::ref(event),
|
||||
mutex.scoped_lock_async(),
|
||||
makeTask(123) | cppcoro::fmap([](int x) { return x + 1; }));
|
||||
|
||||
(void)eventResult;
|
||||
(void)mutexLock;
|
||||
CHECK(number == 124);
|
||||
CHECK(!mutex.try_lock());
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_CASE("when_all() with all task types")
|
||||
{
|
||||
counted::reset_counts();
|
||||
|
||||
auto run = [](cppcoro::async_manual_reset_event& event) -> cppcoro::task<>
|
||||
{
|
||||
using namespace std::string_literals;
|
||||
|
||||
auto[a, b] = co_await cppcoro::when_all(
|
||||
when_event_set_return<cppcoro::task>(event, "foo"s),
|
||||
when_event_set_return<cppcoro::shared_task>(event, counted{}));
|
||||
|
||||
CHECK(a == "foo");
|
||||
CHECK(b.id == 0);
|
||||
CHECK(counted::active_count() == 1);
|
||||
};
|
||||
|
||||
cppcoro::async_manual_reset_event event;
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
run(event),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
event.set();
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_CASE("when_all() throws if any task throws")
|
||||
{
|
||||
struct X {};
|
||||
struct Y {};
|
||||
|
||||
int startedCount = 0;
|
||||
auto makeTask = [&](int value) -> cppcoro::task<int>
|
||||
{
|
||||
++startedCount;
|
||||
if (value == 0) throw X{};
|
||||
else if (value == 1) throw Y{};
|
||||
else co_return value;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait([&]() -> cppcoro::task<>
|
||||
{
|
||||
try
|
||||
{
|
||||
// This could either throw X or Y exception.
|
||||
// The exact exception that is thrown is not defined if multiple tasks throw an exception.
|
||||
// TODO: Consider throwing some kind of aggregate_exception that collects all of the exceptions together.
|
||||
(void)co_await cppcoro::when_all(makeTask(0), makeTask(1), makeTask(2));
|
||||
}
|
||||
catch (const X&)
|
||||
{
|
||||
}
|
||||
catch (const Y&)
|
||||
{
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
TEST_CASE("when_all() with task<void>")
|
||||
{
|
||||
int voidTaskCount = 0;
|
||||
auto makeVoidTask = [&]() -> cppcoro::task<>
|
||||
{
|
||||
++voidTaskCount;
|
||||
co_return;
|
||||
};
|
||||
|
||||
auto makeIntTask = [](int x) -> cppcoro::task<int>
|
||||
{
|
||||
co_return x;
|
||||
};
|
||||
|
||||
// Single void task in when_all()
|
||||
auto[x] = cppcoro::sync_wait(cppcoro::when_all(makeVoidTask()));
|
||||
(void)x;
|
||||
CHECK(voidTaskCount == 1);
|
||||
|
||||
// Multiple void tasks in when_all()
|
||||
auto[a, b] = cppcoro::sync_wait(cppcoro::when_all(
|
||||
makeVoidTask(),
|
||||
makeVoidTask()));
|
||||
(void)a;
|
||||
(void)b;
|
||||
CHECK(voidTaskCount == 3);
|
||||
|
||||
// Mixing void and non-void tasks in when_all()
|
||||
auto[v1, i, v2] = cppcoro::sync_wait(cppcoro::when_all(
|
||||
makeVoidTask(),
|
||||
makeIntTask(123),
|
||||
makeVoidTask()));
|
||||
(void)v1;
|
||||
(void)v2;
|
||||
CHECK(voidTaskCount == 5);
|
||||
|
||||
CHECK(i == 123);
|
||||
}
|
||||
|
||||
TEST_CASE("when_all() with vector<task<>>")
|
||||
{
|
||||
int startedCount = 0;
|
||||
auto makeTask = [&](cppcoro::async_manual_reset_event& event) -> cppcoro::task<>
|
||||
{
|
||||
++startedCount;
|
||||
co_await event;
|
||||
};
|
||||
|
||||
cppcoro::async_manual_reset_event event1;
|
||||
cppcoro::async_manual_reset_event event2;
|
||||
|
||||
bool finished = false;
|
||||
|
||||
auto run = [&]() -> cppcoro::task<>
|
||||
{
|
||||
std::vector<cppcoro::task<>> tasks;
|
||||
tasks.push_back(makeTask(event1));
|
||||
tasks.push_back(makeTask(event2));
|
||||
tasks.push_back(makeTask(event1));
|
||||
|
||||
auto allTask = cppcoro::when_all(std::move(tasks));
|
||||
|
||||
CHECK(startedCount == 0);
|
||||
|
||||
co_await allTask;
|
||||
|
||||
finished = true;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
run(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(startedCount == 3);
|
||||
CHECK(!finished);
|
||||
|
||||
event1.set();
|
||||
|
||||
CHECK(!finished);
|
||||
|
||||
event2.set();
|
||||
|
||||
CHECK(finished);
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
TEST_CASE("when_all() with vector<shared_task<>>")
|
||||
{
|
||||
int startedCount = 0;
|
||||
auto makeTask = [&](cppcoro::async_manual_reset_event& event) -> cppcoro::shared_task<>
|
||||
{
|
||||
++startedCount;
|
||||
co_await event;
|
||||
};
|
||||
|
||||
cppcoro::async_manual_reset_event event1;
|
||||
cppcoro::async_manual_reset_event event2;
|
||||
|
||||
bool finished = false;
|
||||
|
||||
auto run = [&]() -> cppcoro::task<>
|
||||
{
|
||||
std::vector<cppcoro::shared_task<>> tasks;
|
||||
tasks.push_back(makeTask(event1));
|
||||
tasks.push_back(makeTask(event2));
|
||||
tasks.push_back(makeTask(event1));
|
||||
|
||||
auto allTask = cppcoro::when_all(std::move(tasks));
|
||||
|
||||
CHECK(startedCount == 0);
|
||||
|
||||
co_await allTask;
|
||||
|
||||
finished = true;
|
||||
};
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
run(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(startedCount == 3);
|
||||
CHECK(!finished);
|
||||
|
||||
event1.set();
|
||||
|
||||
CHECK(!finished);
|
||||
|
||||
event2.set();
|
||||
|
||||
CHECK(finished);
|
||||
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
template<template<typename T> class TASK>
|
||||
void check_when_all_vector_of_task_value()
|
||||
{
|
||||
cppcoro::async_manual_reset_event event1;
|
||||
cppcoro::async_manual_reset_event event2;
|
||||
|
||||
bool whenAllCompleted = false;
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
std::vector<TASK<int>> tasks;
|
||||
|
||||
tasks.emplace_back(when_event_set_return<TASK>(event1, 1));
|
||||
tasks.emplace_back(when_event_set_return<TASK>(event2, 2));
|
||||
|
||||
auto whenAllTask = cppcoro::when_all(std::move(tasks));
|
||||
|
||||
auto values = co_await whenAllTask;
|
||||
REQUIRE(values.size() == 2);
|
||||
CHECK(values[0] == 1);
|
||||
CHECK(values[1] == 2);
|
||||
|
||||
whenAllCompleted = true;
|
||||
}(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(!whenAllCompleted);
|
||||
event2.set();
|
||||
CHECK(!whenAllCompleted);
|
||||
event1.set();
|
||||
CHECK(whenAllCompleted);
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(CPPCORO_RELEASE_OPTIMISED)
|
||||
constexpr bool isOptimised = true;
|
||||
#else
|
||||
constexpr bool isOptimised = false;
|
||||
#endif
|
||||
|
||||
// Disable test on MSVC x86 optimised due to bad codegen bug in
|
||||
// `co_await whenAllTask` expression under MSVC 15.7 (Preview 2) and earlier.
|
||||
TEST_CASE("when_all() with vector<task<T>>"
|
||||
* doctest::skip(CPPCORO_COMPILER_MSVC && CPPCORO_COMPILER_MSVC <= 191426316 && CPPCORO_CPU_X86 && isOptimised))
|
||||
{
|
||||
check_when_all_vector_of_task_value<cppcoro::task>();
|
||||
}
|
||||
|
||||
// Disable test on MSVC x64 optimised due to bad codegen bug in
|
||||
// 'co_await whenAllTask' expression.
|
||||
// Issue reported to MS on 19/11/2017.
|
||||
TEST_CASE("when_all() with vector<shared_task<T>>"
|
||||
* doctest::skip(CPPCORO_COMPILER_MSVC && CPPCORO_COMPILER_MSVC <= 191225805 &&
|
||||
isOptimised && CPPCORO_CPU_X64))
|
||||
{
|
||||
check_when_all_vector_of_task_value<cppcoro::shared_task>();
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
template<template<typename T> class TASK>
|
||||
void check_when_all_vector_of_task_reference()
|
||||
{
|
||||
cppcoro::async_manual_reset_event event1;
|
||||
cppcoro::async_manual_reset_event event2;
|
||||
|
||||
int value1 = 1;
|
||||
int value2 = 2;
|
||||
|
||||
auto makeTask = [](cppcoro::async_manual_reset_event& event, int& value) -> TASK<int&>
|
||||
{
|
||||
co_await event;
|
||||
co_return value;
|
||||
};
|
||||
|
||||
bool whenAllComplete = false;
|
||||
|
||||
cppcoro::sync_wait(cppcoro::when_all_ready(
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
std::vector<TASK<int&>> tasks;
|
||||
tasks.emplace_back(makeTask(event1, value1));
|
||||
tasks.emplace_back(makeTask(event2, value2));
|
||||
|
||||
auto whenAllTask = cppcoro::when_all(std::move(tasks));
|
||||
|
||||
std::vector<std::reference_wrapper<int>> values = co_await whenAllTask;
|
||||
REQUIRE(values.size() == 2);
|
||||
CHECK(&values[0].get() == &value1);
|
||||
CHECK(&values[1].get() == &value2);
|
||||
|
||||
whenAllComplete = true;
|
||||
}(),
|
||||
[&]() -> cppcoro::task<>
|
||||
{
|
||||
CHECK(!whenAllComplete);
|
||||
event2.set();
|
||||
CHECK(!whenAllComplete);
|
||||
event1.set();
|
||||
CHECK(whenAllComplete);
|
||||
co_return;
|
||||
}()));
|
||||
}
|
||||
}
|
||||
|
||||
// Disable test on MSVC x64 optimised due to bad codegen bug in
|
||||
// 'co_await whenAllTask' expression.
|
||||
// Issue reported to MS on 19/11/2017.
|
||||
TEST_CASE("when_all() with vector<task<T&>>"
|
||||
* doctest::skip(CPPCORO_COMPILER_MSVC && CPPCORO_COMPILER_MSVC <= 191225805 &&
|
||||
isOptimised && CPPCORO_CPU_X64))
|
||||
{
|
||||
check_when_all_vector_of_task_reference<cppcoro::task>();
|
||||
}
|
||||
|
||||
// Disable test on MSVC x64 optimised due to bad codegen bug in
|
||||
// 'co_await whenAllTask' expression.
|
||||
// Issue reported to MS on 19/11/2017.
|
||||
TEST_CASE("when_all() with vector<shared_task<T&>>"
|
||||
* doctest::skip(CPPCORO_COMPILER_MSVC && CPPCORO_COMPILER_MSVC <= 191225805 &&
|
||||
isOptimised && CPPCORO_CPU_X64))
|
||||
{
|
||||
check_when_all_vector_of_task_reference<cppcoro::shared_task>();
|
||||
}
|
||||
|
||||
TEST_SUITE_END();
|
||||
Reference in New Issue
Block a user