This commit is contained in:
jeanlemotan
2024-07-02 18:13:47 +02:00
commit bbeaa887cd
173 changed files with 34365 additions and 0 deletions
+62
View File
@@ -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()
+140
View File
@@ -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();
+330
View File
@@ -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();
+113
View File
@@ -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();
+96
View File
@@ -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();
+90
View File
@@ -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();
+93
View File
@@ -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,
)
+342
View File
@@ -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();
+11
View File
@@ -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;
+42
View File
@@ -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
+12
View File
@@ -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
+175
View File
@@ -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
+81
View File
@@ -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}")
+215
View File
@@ -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();
+412
View File
@@ -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();
+71
View File
@@ -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
+230
View File
@@ -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();
+45
View File
@@ -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();
+56
View File
@@ -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();
+90
View File
@@ -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();
+33
View File
@@ -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();
+154
View File
@@ -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();
+55
View File
@@ -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();
+7
View File
@@ -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"
+205
View File
@@ -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();
+424
View File
@@ -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();
+290
View File
@@ -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();
+213
View File
@@ -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();
+248
View File
@@ -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();
+95
View File
@@ -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();
+474
View File
@@ -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();
+290
View File
@@ -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();
+76
View File
@@ -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();
+349
View File
@@ -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();
+265
View File
@@ -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();
+427
View File
@@ -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();