Change tests to use doctest testing framework.

Break out tests to separate files per abstraction.
This commit is contained in:
Lewis Baker 2017-05-22 06:34:19 +09:30
parent 5e01767869
commit 9a620aa4c5
12 changed files with 7431 additions and 1724 deletions

View file

@ -0,0 +1,273 @@
///////////////////////////////////////////////////////////////////////////////
// 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 "doctest/doctest.h"
TEST_CASE("default-constructed async_generator is an empty sequence")
{
[]() -> cppcoro::task<>
{
// Iterating over default-constructed async_generator just
// gives an empty sequence.
cppcoro::async_generator<int> g;
for co_await(int x : g)
{
CHECK(false);
}
}();
}
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")
{
bool startedExecution = false;
auto gen = [&]() -> cppcoro::async_generator<std::uint32_t>
{
startedExecution = true;
co_yield 1;
}();
CHECK(!startedExecution);
auto itAwaitable = gen.begin();
CHECK(startedExecution);
CHECK(itAwaitable.await_ready());
auto it = itAwaitable.await_resume();
CHECK(it != gen.end());
CHECK(*it == 1u);
}
TEST_CASE("enumerate sequence of multiple values")
{
bool startedExecution = false;
auto gen = [&]() -> cppcoro::async_generator<std::uint32_t>
{
startedExecution = true;
co_yield 1;
co_yield 2;
co_yield 3;
}();
CHECK(!startedExecution);
auto beginAwaitable = gen.begin();
CHECK(startedExecution);
CHECK(beginAwaitable.await_ready());
auto it = beginAwaitable.await_resume();
CHECK(it != gen.end());
CHECK(*it == 1u);
auto incrementAwaitable1 = ++it;
CHECK(incrementAwaitable1.await_ready());
incrementAwaitable1.await_resume();
CHECK(*it == 2u);
auto incrementAwaitable2 = ++it;
CHECK(incrementAwaitable2.await_ready());
incrementAwaitable2.await_resume();
CHECK(*it == 3u);
auto incrementAwaitable3 = ++it;
CHECK(incrementAwaitable3.await_ready());
incrementAwaitable3.await_resume();
CHECK(it == gen.end());
}
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")
{
bool aDestructed = false;
bool bDestructed = false;
{
auto gen = [&](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;
}(&aDestructed);
CHECK(!aDestructed);
CHECK(!bDestructed);
auto beginOp = gen.begin();
CHECK(beginOp.await_ready());
CHECK(!aDestructed);
CHECK(!bDestructed);
auto it = beginOp.await_resume();
CHECK(*it == 1u);
CHECK(!aDestructed);
CHECK(!bDestructed);
}
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 producer = [&]() -> cppcoro::async_generator<std::uint32_t>
{
co_await p1;
co_yield 1;
co_await p2;
co_yield 2;
co_await p3;
}();
auto consumer = [&]() -> cppcoro::task<>
{
auto it = co_await producer.begin();
CHECK(*it == 1u);
co_await ++it;
CHECK(*it == 2u);
co_await c1;
co_await ++it;
CHECK(it == producer.end());
}();
p1.set();
p2.set();
c1.set();
CHECK(!consumer.is_ready());
p3.set();
CHECK(consumer.is_ready());
}
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);
auto beginAwaitable = gen.begin();
CHECK(beginAwaitable.await_ready());
CHECK_THROWS_AS(beginAwaitable.await_resume(), 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);
auto beginAwaitable = gen.begin();
CHECK(beginAwaitable.await_ready());
auto it = beginAwaitable.await_resume();
CHECK(*it == 1u);
auto incrementAwaitable = ++it;
CHECK(incrementAwaitable.await_ready());
CHECK_THROWS_AS(incrementAwaitable.await_resume(), const TestException&);
CHECK(it == gen.end());
}
TEST_CASE("large number of synchronous completions doesn't result in stack-overflow")
{
cppcoro::single_consumer_event event;
auto sequence = [](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;
}
}(event);
auto consumerTask = [](cppcoro::async_generator<std::uint32_t> sequence) -> cppcoro::task<>
{
std::uint32_t expected = 0;
for co_await(std::uint32_t i : sequence)
{
CHECK(i == expected++);
}
CHECK(expected == 1'000'000u);
}(std::move(sequence));
CHECK(!consumerTask.is_ready());
// 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();
CHECK(consumerTask.is_ready());
}

View file

@ -0,0 +1,78 @@
///////////////////////////////////////////////////////////////////////////////
// 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>
// HACK: Include <ostream> here to workaround bug in doctest.h
// See https://github.com/onqtam/doctest/issues/72
#include <ostream>
#include "doctest/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());
}
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<>
{
cppcoro::async_mutex_lock lock = co_await mutex.lock_async();
co_await e;
++value;
};
auto t1 = f(a);
CHECK(!t1.is_ready());
CHECK(value == 0);
auto t2 = f(b);
auto t3 = f(c);
a.set();
CHECK(value == 1);
auto t4 = f(d);
b.set();
CHECK(value == 2);
c.set();
CHECK(value == 3);
d.set();
CHECK(value == 4);
CHECK(t1.is_ready());
CHECK(t2.is_ready());
CHECK(t3.is_ready());
CHECK(t4.is_ready());
}
TEST_SUITE_END();

View file

@ -5,22 +5,38 @@
import cake.path
from cake.tools import script, env, compiler, project
from cake.tools import script, env, compiler, project, variant
script.include([
env.expand('${CPPCORO}/lib/use.cake'),
])
headers = script.cwd([
"counted.hpp",
])
sources = script.cwd([
'main.cpp',
'counted.cpp',
'async_generator_tests.cpp',
'async_mutex_tests.cpp',
'cancellation_token_tests.cpp',
'lazy_task_tests.cpp',
'shared_lazy_task_tests.cpp',
'shared_task_tests.cpp',
'task_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=env.expand('${CPPCORO_BUILD}/test'),
targetDir=intermediateBuildDir,
sources=sources,
)
@ -32,7 +48,7 @@ testExe = compiler.program(
vcproj = project.project(
target=env.expand('${CPPCORO_PROJECT}/cppcoro_tests'),
items={
'Source': sources,
'Source': sources + headers,
'': extras,
},
output=testExe,

View file

@ -0,0 +1,341 @@
///////////////////////////////////////////////////////////////////////////////
// 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>
// HACK: Including <ostream> here to work around bug in doctest.h
// See https://github.com/onqtam/doctest/issues/72
#include <ostream>
#include "doctest/doctest.h"
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);
}

11
test/counted.cpp Normal file
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
test/counted.hpp Normal file
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

5717
test/doctest/doctest.h Normal file

File diff suppressed because it is too large Load diff

194
test/lazy_task_tests.cpp Normal file
View file

@ -0,0 +1,194 @@
///////////////////////////////////////////////////////////////////////////////
// Copyright (c) Lewis Baker
// Licenced under MIT license. See LICENSE.txt for details.
///////////////////////////////////////////////////////////////////////////////
#include <cppcoro/lazy_task.hpp>
#include <cppcoro/task.hpp>
#include <cppcoro/single_consumer_event.hpp>
#include "counted.hpp"
#include <cassert>
#include <type_traits>
#include "doctest/doctest.h"
TEST_SUITE_BEGIN("lazy_task");
TEST_CASE("lazy_task doesn't start until awaited")
{
bool started = false;
auto func = [&]() -> cppcoro::lazy_task<>
{
started = true;
co_return;
};
auto t = func();
CHECK(!started);
auto consumer = [&]() -> cppcoro::task<>
{
co_await t;
};
auto consumerTask = consumer();
CHECK(started);
}
TEST_CASE("awaiting default-constructed lazy_task throws broken_promise")
{
[&]() -> cppcoro::task<>
{
cppcoro::lazy_task<> t;
CHECK_THROWS_AS(co_await t, const cppcoro::broken_promise&);
}();
}
TEST_CASE("awaiting lazy_task that completes asynchronously")
{
bool reachedBeforeEvent = false;
bool reachedAfterEvent = false;
cppcoro::single_consumer_event event;
auto f = [&]() -> cppcoro::lazy_task<>
{
reachedBeforeEvent = true;
co_await event;
reachedAfterEvent = true;
};
auto t = f();
CHECK(!t.is_ready());
CHECK(!reachedBeforeEvent);
auto t2 = [](cppcoro::lazy_task<>& t) -> cppcoro::task<>
{
co_await t;
}(t);
CHECK(!t2.is_ready());
event.set();
CHECK(t.is_ready());
CHECK(t2.is_ready());
CHECK(reachedAfterEvent);
}
TEST_CASE("destroying lazy_task that was never awaited destroys captured args")
{
counted::reset_counts();
auto f = [](counted c) -> cppcoro::lazy_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("lazy_task destructor destroys result")
{
counted::reset_counts();
auto f = []() -> cppcoro::lazy_task<counted>
{
co_return counted{};
};
{
auto t = f();
CHECK(counted::active_count() == 0);
[](cppcoro::lazy_task<counted>& t) -> cppcoro::task<>
{
co_await t;
CHECK(t.is_ready());
CHECK(counted::active_count() == 1);
}(t);
CHECK(counted::active_count() == 1);
}
CHECK(counted::active_count() == 0);
}
TEST_CASE("lazy_task of reference type")
{
int value = 3;
auto f = [&]() -> cppcoro::lazy_task<int&>
{
co_return value;
};
auto g = [&]() -> 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 lazy_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 lazy_task<int&> should result in an int&");
CHECK(&result == &value);
}
};
auto t = g();
CHECK(t.is_ready());
}
TEST_CASE("passing parameter by value to lazy_task coroutine calls move-constructor exactly once")
{
counted::reset_counts();
auto f = [](counted arg) -> cppcoro::lazy_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_SUITE_END();

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,205 @@
///////////////////////////////////////////////////////////////////////////////
// Copyright (c) Lewis Baker
// Licenced under MIT license. See LICENSE.txt for details.
///////////////////////////////////////////////////////////////////////////////
#include <cppcoro/shared_lazy_task.hpp>
#include <cppcoro/task.hpp>
#include <cppcoro/single_consumer_event.hpp>
#include "counted.hpp"
#include "doctest/doctest.h"
TEST_SUITE_BEGIN("shared_lazy_task");
TEST_CASE("awaiting default-constructed task throws broken_promise")
{
auto t = []() -> cppcoro::task<>
{
CHECK_THROWS_AS(co_await cppcoro::shared_lazy_task<>{}, const cppcoro::broken_promise&);
}();
CHECK(t.is_ready());
}
TEST_CASE("coroutine doesn't start executing until awaited")
{
bool startedExecuting = false;
auto f = [&]() -> cppcoro::shared_lazy_task<>
{
startedExecuting = true;
co_return;
};
auto t = f();
CHECK(!t.is_ready());
CHECK(!startedExecuting);
auto waiter = [](cppcoro::shared_lazy_task<> t) -> cppcoro::task<>
{
co_await t;
}(t);
CHECK(waiter.is_ready());
CHECK(t.is_ready());
CHECK(startedExecuting);
}
TEST_CASE("result is destroyed when last reference is destroyed")
{
counted::reset_counts();
{
auto t = []() -> cppcoro::shared_lazy_task<counted>
{
co_return counted{};
}();
CHECK(counted::active_count() == 0);
[](cppcoro::shared_lazy_task<counted> t) -> cppcoro::task<>
{
co_await t;
}(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_lazy_task<int>
{
startedExecution = true;
co_await event;
co_return 1;
};
auto consume = [](cppcoro::shared_lazy_task<int> t) -> cppcoro::task<>
{
int result = co_await t;
CHECK(result == 1);
};
auto sharedTask = produce();
CHECK(!sharedTask.is_ready());
CHECK(!startedExecution);
auto t1 = consume(sharedTask);
CHECK(!t1.is_ready());
CHECK(startedExecution);
CHECK(!sharedTask.is_ready());
auto t2 = consume(sharedTask);
CHECK(!t2.is_ready());
CHECK(!sharedTask.is_ready());
auto t3 = consume(sharedTask);
event.set();
CHECK(sharedTask.is_ready());
CHECK(t1.is_ready());
CHECK(t2.is_ready());
CHECK(t3.is_ready());
}
TEST_CASE("waiting on shared_lazy_task in loop doesn't cause stack-overflow")
{
// This test checks that awaiting a shared_lazy_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_lazy_task<int>
{
co_return 1;
};
auto run = [&]() -> cppcoro::task<>
{
int result = 0;
for (int i = 0; i < 1'000'000; ++i)
{
result += co_await completesSynchronously();
}
CHECK(result == 1'000'000);
};
auto t = run();
CHECK(t.is_ready());
}
TEST_CASE("make_shared_task")
{
bool startedExecution = false;
auto f = [&]() -> cppcoro::lazy_task<std::string>
{
startedExecution = false;
co_return "test";
};
auto t = f();
cppcoro::shared_lazy_task<std::string> sharedT =
cppcoro::make_shared_task(std::move(t));
CHECK(!sharedT.is_ready());
CHECK(!startedExecution);
auto consume = [](cppcoro::shared_lazy_task<std::string> t) -> cppcoro::task<>
{
auto x = co_await std::move(t);
CHECK(x == "test");
};
auto c1 = consume(sharedT);
auto c2 = consume(sharedT);
CHECK(c1.is_ready());
CHECK(c2.is_ready());
}
TEST_CASE("make_shared_task of void"
* doctest::description{ "Tests that workaround for MSVC 2017.1 bug is operational" })
{
bool startedExecution = false;
auto f = [&]() -> cppcoro::lazy_task<>
{
startedExecution = true;
co_return;
};
auto t = f();
cppcoro::shared_lazy_task<> sharedT = cppcoro::make_shared_task(std::move(t));
CHECK(!sharedT.is_ready());
CHECK(!startedExecution);
auto consume = [](cppcoro::shared_lazy_task<> t) -> cppcoro::task<>
{
co_await t;
};
auto c1 = consume(sharedT);
CHECK(startedExecution);
auto c2 = consume(sharedT);
CHECK(c1.is_ready());
CHECK(c2.is_ready());
}
TEST_SUITE_END();

275
test/shared_task_tests.cpp Normal file
View file

@ -0,0 +1,275 @@
///////////////////////////////////////////////////////////////////////////////
// Copyright (c) Lewis Baker
// Licenced under MIT license. See LICENSE.txt for details.
///////////////////////////////////////////////////////////////////////////////
#include <cppcoro/shared_task.hpp>
#include <cppcoro/task.hpp>
#include <cppcoro/single_consumer_event.hpp>
#include "counted.hpp"
#include <string>
#include "doctest/doctest.h"
TEST_SUITE_BEGIN("shared_task");
TEST_CASE("default constructed shared_task")
{
SUBCASE("is_ready")
{
cppcoro::shared_task<> t;
CHECK(t.is_ready());
cppcoro::shared_task<> tCopy = t;
CHECK(t.is_ready());
}
SUBCASE("awaiting throws broken_promise")
{
auto task = []() -> cppcoro::task<>
{
CHECK_THROWS_AS(co_await cppcoro::shared_task<>{}, const cppcoro::broken_promise&);
}();
CHECK(task.is_ready());
}
}
TEST_CASE("multiple waiters")
{
cppcoro::single_consumer_event event;
auto sharedTask = [](cppcoro::single_consumer_event& event) -> cppcoro::shared_task<>
{
co_await event;
}(event);
CHECK(!sharedTask.is_ready());
auto consumeTask = [](cppcoro::shared_task<> task) -> cppcoro::task<>
{
co_await task;
};
auto t1 = consumeTask(sharedTask);
auto t2 = consumeTask(sharedTask);
CHECK(!t1.is_ready());
CHECK(!t2.is_ready());
event.set();
CHECK(sharedTask.is_ready());
CHECK(t1.is_ready());
CHECK(t2.is_ready());
auto t3 = consumeTask(sharedTask);
CHECK(t3.is_ready());
}
TEST_CASE("unhandled exception is rethrown")
{
class X {};
auto throwingTask = []() -> cppcoro::shared_task<>
{
co_await std::experimental::suspend_never{};
throw X{};
};
[&]() -> cppcoro::task<>
{
auto t = throwingTask();
CHECK(t.is_ready());
CHECK_THROWS_AS(co_await t, const X&);
}();
}
TEST_CASE("result is destroyed when last reference is destroyed")
{
counted::reset_counts();
{
cppcoro::shared_task<counted> tCopy;
{
auto t = []() -> cppcoro::shared_task<counted>
{
co_return counted{};
}();
CHECK(t.is_ready());
tCopy = t;
CHECK(tCopy.is_ready());
}
{
cppcoro::shared_task<counted> tCopy2 = tCopy;
CHECK(tCopy2.is_ready());
}
CHECK(counted::active_count() == 1);
}
CHECK(counted::active_count() == 0);
}
TEST_CASE("assigning result from shared_task doesn't move result")
{
auto f = []() -> cppcoro::shared_task<std::string>
{
co_return "string that is longer than short-string optimisation";
};
auto t = f();
auto g = [](cppcoro::shared_task<std::string> t) -> cppcoro::task<>
{
auto x = co_await t;
CHECK(x == "string that is longer than short-string optimisation");
auto y = co_await std::move(t);
CHECK(y == "string that is longer than short-string optimisation");
};
g(t);
g(t);
}
TEST_CASE("shared_task of reference type")
{
const std::string value = "some string value";
auto f = [&]() -> cppcoro::shared_task<const std::string&>
{
co_return value;
};
[&]() -> cppcoro::task<>
{
auto& result = co_await f();
CHECK(&result == &value);
}();
}
TEST_CASE("shared_task returning rvalue reference moves into promise")
{
counted::reset_counts();
auto f = []() -> cppcoro::shared_task<counted>
{
co_return counted{};
};
CHECK(counted::active_count() == 0);
{
auto t = f();
CHECK(counted::default_construction_count == 1);
CHECK(counted::copy_construction_count == 0);
CHECK(counted::move_construction_count == 1);
CHECK(counted::destruction_count == 1);
CHECK(counted::active_count() == 1);
// Moving task doesn't move/copy result.
auto t2 = std::move(t);
CHECK(counted::default_construction_count == 1);
CHECK(counted::copy_construction_count == 0);
CHECK(counted::move_construction_count == 1);
CHECK(counted::destruction_count == 1);
CHECK(counted::active_count() == 1);
}
CHECK(counted::active_count() == 0);
}
TEST_CASE("equality comparison")
{
auto f = []() -> cppcoro::shared_task<>
{
co_return;
};
cppcoro::shared_task<> t0;
cppcoro::shared_task<> t1 = t0;
cppcoro::shared_task<> t2 = f();
cppcoro::shared_task<> t3 = t2;
cppcoro::shared_task<> t4 = f();
CHECK(t0 == t0);
CHECK(t0 == t1);
CHECK(t0 != t2);
CHECK(t0 != t3);
CHECK(t0 != t4);
CHECK(t2 == t2);
CHECK(t2 == t3);
CHECK(t2 != t4);
}
TEST_CASE("make_shared_task")
{
cppcoro::single_consumer_event event;
auto f = [&]() -> cppcoro::task<std::string>
{
co_await event;
co_return "foo";
};
auto t = cppcoro::make_shared_task(f());
auto consumer = [](cppcoro::shared_task<std::string> task) -> cppcoro::task<>
{
CHECK(co_await task == "foo");
};
auto consumerTask0 = consumer(t);
auto consumerTask1 = consumer(t);
CHECK(!consumerTask0.is_ready());
CHECK(!consumerTask1.is_ready());
event.set();
CHECK(consumerTask0.is_ready());
CHECK(consumerTask1.is_ready());
}
TEST_CASE("make_shared_task of void-returning task"
* doctest::description{"checks that workaround for MSVC bug is getting picked up."
"MSVC 2017.1 was failing to evaluate <expr> in 'co_return <expr>' if expression was void."})
{
cppcoro::single_consumer_event event;
auto f = [&]() -> cppcoro::task<>
{
co_await event;
};
auto t = cppcoro::make_shared_task(f());
CHECK(!t.is_ready());
auto consumer = [](cppcoro::shared_task<> task) -> cppcoro::task<>
{
co_await task;
};
auto consumerTask0 = consumer(t);
auto consumerTask1 = consumer(t);
CHECK(!consumerTask0.is_ready());
CHECK(!consumerTask1.is_ready());
event.set();
CHECK(consumerTask0.is_ready());
CHECK(consumerTask1.is_ready());
}
TEST_SUITE_END();

274
test/task_tests.cpp Normal file
View file

@ -0,0 +1,274 @@
///////////////////////////////////////////////////////////////////////////////
// Copyright (c) Lewis Baker
// Licenced under MIT license. See LICENSE.txt for details.
///////////////////////////////////////////////////////////////////////////////
#include <cppcoro/task.hpp>
#include <cppcoro/single_consumer_event.hpp>
#include "counted.hpp"
#include "doctest/doctest.h"
TEST_SUITE_BEGIN("task");
TEST_CASE("default constructed task")
{
cppcoro::task<> t;
CHECK(t.is_ready());
SUBCASE("throws broken_promise when awaited")
{
auto op = t.operator co_await();
CHECK(op.await_ready());
CHECK_THROWS_AS(op.await_resume(), const cppcoro::broken_promise&);
}
}
TEST_CASE("co_await synchronously completing task")
{
auto doNothingAsync = []() -> cppcoro::task<>
{
co_return;
};
auto task = doNothingAsync();
CHECK(task.is_ready());
bool ok = false;
auto test = [&]() -> cppcoro::task<>
{
co_await task;
ok = true;
};
test();
CHECK(ok);
}
TEST_CASE("task of move-only type by value")
{
// unique_ptr is move-only type.
auto getIntPtrAsync = []() -> cppcoro::task<std::unique_ptr<int>>
{
co_return std::make_unique<int>(123);
};
SUBCASE("co_await temporary")
{
auto test = [&]() -> cppcoro::task<>
{
auto intPtr = co_await getIntPtrAsync();
REQUIRE(intPtr);
CHECK(*intPtr == 123);
};
test();
}
SUBCASE("co_await lvalue reference")
{
auto test = [&]() -> cppcoro::task<>
{
// co_await yields l-value reference if task is l-value
auto intPtrTask = getIntPtrAsync();
auto& intPtr = co_await intPtrTask;
REQUIRE(intPtr);
CHECK(*intPtr == 123);
};
test();
}
SUBCASE("co_await rvalue reference")
{
auto test = [&]() -> cppcoro::task<>
{
// Returns r-value reference if task is r-value
auto intPtrTask = getIntPtrAsync();
auto intPtr = co_await std::move(intPtrTask);
REQUIRE(intPtr);
CHECK(*intPtr == 123);
};
test();
}
}
TEST_CASE("task of reference type")
{
int value = 0;
auto getRefAsync = [&]() -> cppcoro::task<int&>
{
co_return value;
};
auto test = [&]() -> cppcoro::task<>
{
// Await r-value task results in l-value reference
decltype(auto) result = co_await getRefAsync();
CHECK(&result == &value);
// Await l-value task results in l-value reference
auto getRefTask = getRefAsync();
decltype(auto) result2 = co_await getRefTask;
CHECK(&result2 == &value);
};
auto task = test();
CHECK(task.is_ready());
}
TEST_CASE("task of value-type moves into promise if passed rvalue reference")
{
counted::reset_counts();
auto f = []() -> cppcoro::task<counted>
{
co_return counted{};
};
CHECK(counted::active_count() == 0);
{
auto t = f();
CHECK(counted::default_construction_count == 1);
CHECK(counted::copy_construction_count == 0);
CHECK(counted::move_construction_count == 1);
CHECK(counted::destruction_count == 1);
CHECK(counted::active_count() == 1);
// Moving task doesn't move/copy result.
auto t2 = std::move(t);
CHECK(counted::default_construction_count == 1);
CHECK(counted::copy_construction_count == 0);
CHECK(counted::move_construction_count == 1);
CHECK(counted::destruction_count == 1);
CHECK(counted::active_count() == 1);
}
CHECK(counted::active_count() == 0);
}
TEST_CASE("task of value-type copies into promise if passed lvalue reference")
{
counted::reset_counts();
auto f = []() -> cppcoro::task<counted>
{
counted temp;
// Should be calling copy-constructor here since <promise>.return_value()
// is being passed an l-value reference.
co_return temp;
};
CHECK(counted::active_count() == 0);
{
auto t = f();
CHECK(counted::default_construction_count == 1);
CHECK(counted::copy_construction_count == 1);
CHECK(counted::move_construction_count == 0);
CHECK(counted::destruction_count == 1);
CHECK(counted::active_count() == 1);
// Moving the task doesn't move/copy the result
auto t2 = std::move(t);
CHECK(counted::default_construction_count == 1);
CHECK(counted::copy_construction_count == 1);
CHECK(counted::move_construction_count == 0);
CHECK(counted::destruction_count == 1);
CHECK(counted::active_count() == 1);
}
CHECK(counted::active_count() == 0);
}
TEST_CASE("co_await chain of async completions")
{
cppcoro::single_consumer_event event;
bool reachedPointA = false;
bool reachedPointB = false;
auto async1 = [&]() -> cppcoro::task<int>
{
reachedPointA = true;
co_await event;
reachedPointB = true;
co_return 1;
};
bool reachedPointC = false;
bool reachedPointD = false;
auto async2 = [&]() -> cppcoro::task<int>
{
reachedPointC = true;
int result = co_await async1();
reachedPointD = true;
co_return result;
};
auto task = async2();
CHECK(!task.is_ready());
CHECK(reachedPointA);
CHECK(!reachedPointB);
CHECK(reachedPointC);
CHECK(!reachedPointD);
event.set();
CHECK(task.is_ready());
CHECK(reachedPointB);
CHECK(reachedPointD);
[](cppcoro::task<int> t) -> cppcoro::task<>
{
int value = co_await t;
CHECK(value == 1);
}(std::move(task));
}
TEST_CASE("awaiting default-constructed task throws broken_promise")
{
[]() -> cppcoro::task<>
{
cppcoro::task<> broken;
CHECK_THROWS_AS(co_await broken, const cppcoro::broken_promise&);
}();
}
TEST_CASE("awaiting task that completes with exception")
{
class X {};
auto run = [](bool doThrow = true) -> cppcoro::task<>
{
if (doThrow) throw X{};
co_return;
};
auto t = run();
CHECK(t.is_ready());
auto consumeT = [&]() -> cppcoro::task<>
{
SUBCASE("co_await task rethrows exception")
{
CHECK_THROWS_AS(co_await t, const X&);
}
SUBCASE("co_await task.when_ready() doesn't rethrow exception")
{
CHECK_NOTHROW(co_await t.when_ready());
}
};
auto consumer = consumeT();
CHECK(consumer.is_ready());
}
TEST_SUITE_END();