Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions include/cppcoro/generator.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <exception>
#include <iterator>
#include <functional>
#include <optional>

namespace cppcoro
{
Expand All @@ -35,18 +36,30 @@ namespace cppcoro
constexpr cppcoro::suspend_always initial_suspend() const noexcept { return {}; }
constexpr cppcoro::suspend_always final_suspend() const noexcept { return {}; }

template<
typename U = T,
std::enable_if_t<!std::is_rvalue_reference<U>::value, int> = 0>
cppcoro::suspend_always yield_value(std::remove_reference_t<T>& value) noexcept
{
m_value = std::addressof(value);
return {};
}

cppcoro::suspend_always yield_value(std::remove_reference_t<T>&& value) noexcept
template<typename U>
cppcoro::suspend_always yield_value(U&& value) noexcept
{
m_value = std::addressof(value);
if constexpr (std::is_lvalue_reference_v<T>)
{
if constexpr (std::is_lvalue_reference_v<U>)
{
// Yielding an lvalue for a generator of lvalue-reference: just reference it.
m_storage = std::nullopt;
m_value = std::addressof(value);
}
else
{
// Yielding an rvalue for a generator of lvalue-reference: materialise to extend lifetime.
m_storage.emplace(std::forward<U>(value));
m_value = std::addressof(*m_storage);
}
}
else
{
// For value- or rvalue-reference-yielding generators, always materialise.
m_storage.emplace(std::forward<U>(value));
m_value = std::addressof(*m_storage);
}
return {};
}

Expand Down Expand Up @@ -80,6 +93,8 @@ namespace cppcoro

pointer_type m_value{};
std::exception_ptr m_exception{};
// For non-reference T we may need to materialise rvalues across suspension.
std::optional<value_type> m_storage{};

};

Expand Down Expand Up @@ -150,6 +165,8 @@ namespace cppcoro
return m_coroutine.promise().value();
}

template<typename R = reference,
std::enable_if_t<std::is_lvalue_reference_v<R>, int> = 0>
pointer operator->() const noexcept
{
return std::addressof(operator*());
Expand Down
77 changes: 77 additions & 0 deletions test/generator_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,23 @@
#include <string>
#include <forward_list>
#include <cstdint>
#include <type_traits>
#include <utility>

#include "doctest/cppcoro_doctest.h"

TEST_SUITE_BEGIN("generator");

using cppcoro::generator;

namespace {
// Helper to detect presence of operator-> on iterator type
template<typename It, typename = void>
struct has_arrow : std::false_type {};
template<typename It>
struct has_arrow<It, std::void_t<decltype(std::declval<const It&>().operator->())>> : std::true_type {};
}

TEST_CASE("default-constructed generator is empty sequence")
{
generator<int> ints;
Expand Down Expand Up @@ -129,6 +139,73 @@ TEST_CASE("value-category of fmap() matches reference type")
consume([]() -> generator<const int&&> { co_yield 123; }() | fmap(checkIsConstRvalue));
}

TEST_CASE("iterator arrow for lvalue yields and disabled for rvalue yields")
{
SUBCASE("arrow works for generator<int>")
{
auto g = []() -> generator<int>
{
co_yield 10;
}();
auto it = g.begin();
REQUIRE(it != g.end());
auto p = it.operator->();
CHECK(p == &*it);
CHECK(*p == 10);
}

SUBCASE("arrow works for generator<int&>")
{
int x = 42;
auto g = [&]() -> generator<int&>
{
co_yield x;
}();
auto it = g.begin();
REQUIRE(it != g.end());
CHECK(it.operator->() == &x);
CHECK(*it == 42);
}

SUBCASE("arrow is disabled for generator<int&&>")
{
using It = generator<int&&>::iterator;
static_assert(!has_arrow<It>::value, "operator-> must be disabled for rvalue reference yields");
// Still ensure rvalue-yielding generator produces correct values
auto g = []() -> generator<int&&>
{
int v = 7;
co_yield std::move(v);
}();
int count = 0;
for (auto&& v : g)
{
CHECK(v == 7);
++count;
}
CHECK(count == 1);
}
}

TEST_CASE("yielding rvalues produces stable values across suspension")
{
auto make = []() -> generator<std::string>
{
co_yield std::string{"a"};
co_yield std::string{"bc"};
};

auto gen = make();
auto it = gen.begin();
REQUIRE(it != gen.end());
CHECK(*it == std::string{"a"});
++it;
REQUIRE(it != gen.end());
CHECK(*it == std::string{"bc"});
++it;
CHECK(it == gen.end());
}

TEST_CASE("generator doesn't start until its called")
{
bool reachedA = false;
Expand Down
62 changes: 53 additions & 9 deletions test/ip_address_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include <cppcoro/net/ip_address.hpp>

#include "doctest/cppcoro_doctest.h"
#include <string_view>

TEST_SUITE_BEGIN("ip_address");

Expand All @@ -31,15 +32,58 @@ TEST_CASE("to_string")

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 });
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_CASE("round-trip and ordering")
{
// Round-trip: to_string(from_string(s)) yields canonical string, and parsing back preserves value
auto check_round_trip = [](std::string_view s) {
auto p = ip_address::from_string(s);
REQUIRE(p.has_value());
auto canon = p->to_string();
auto p2 = ip_address::from_string(canon);
REQUIRE(p2.has_value());
CHECK(*p == *p2);
// Canonical string should be stable
CHECK(p2->to_string() == canon);
};

// IPv4
check_round_trip("0.0.0.0");
check_round_trip("255.255.255.255");

// IPv6 (mixed case input should parse and normalise to lower-case hex without leading zeros)
check_round_trip("::");
check_round_trip("::1");
check_round_trip("FFFF:0000:0000:0000:0000:0000:0000:0001");
check_round_trip("::192.168.0.1");

// Ordering: IPv4 sorts less than IPv6
ip_address v4 = ipv4_address{127, 0, 0, 1};
ip_address v6 = ipv6_address{0, 0, 0, 0, 0, 0, 0, 1}; // ::1
CHECK(v4 < v6);

// Ordering within same family
CHECK(ipv4_address{1, 1, 1, 1} < ipv4_address{1, 1, 1, 2});
CHECK(ipv6_address{0, 0, 0, 0, 0, 0, 0, 1} < ipv6_address{0, 0, 0, 0, 0, 0, 0, 2});

// Reject trailing/leading whitespace
CHECK(ip_address::from_string("192.168.0.1 ") == std::nullopt);
CHECK(ip_address::from_string("\t::1") == std::nullopt);

// Reject malformed IPv6
CHECK(ip_address::from_string("::::") == std::nullopt);
CHECK(ip_address::from_string("gggg::1") == std::nullopt);
CHECK(ip_address::from_string("12345::1") == std::nullopt);
}

TEST_SUITE_END();
30 changes: 30 additions & 0 deletions test/ip_endpoint_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,35 @@ TEST_CASE("from_string" * doctest::skip{ isMsvc15_5X86Optimised })
443 });
}

TEST_CASE("round-trip and ordering" * doctest::skip{ isMsvc15_5X86Optimised })
{
// Round-trip
auto check_round_trip = [](const char* s) {
auto p = ip_endpoint::from_string(s);
REQUIRE(p.has_value());
auto canon = p->to_string();
auto p2 = ip_endpoint::from_string(canon);
REQUIRE(p2.has_value());
CHECK(*p == *p2);
CHECK(p2->to_string() == canon);
};

check_round_trip("192.168.2.254:80");
check_round_trip("[2001:db8:85a3::8a2e:370:7334]:22");

// Invalid ports/whitespace
CHECK(ip_endpoint::from_string("192.168.1.1:65536") == std::nullopt);
CHECK(ip_endpoint::from_string("[::1]:65536") == std::nullopt);
CHECK(ip_endpoint::from_string(" 192.168.1.1:80") == std::nullopt);
CHECK(ip_endpoint::from_string("[::1]:80 ") == std::nullopt);

// Ordering: IPv4 < IPv6, and within family compare address/port
ip_endpoint v4a = ipv4_endpoint{ ipv4_address{127,0,0,1}, 1 };
ip_endpoint v4b = ipv4_endpoint{ ipv4_address{127,0,0,1}, 2 };
ip_endpoint v6 = ipv6_endpoint{ ipv6_address{0,0,0,0,0,0,0,1}, 1 };
CHECK(v4a < v6);
CHECK(v4a < v4b);
}

TEST_SUITE_END();

27 changes: 27 additions & 0 deletions test/ipv4_address_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,31 @@ TEST_CASE("from_string")
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_CASE("round-trip and ordering")
{
// Round-trip canonicalisation via to_string()/from_string()
auto check_round_trip = [](const char* s) {
auto p = ipv4_address::from_string(s);
REQUIRE(p.has_value());
auto canon = p->to_string();
auto p2 = ipv4_address::from_string(canon);
REQUIRE(p2.has_value());
CHECK(*p == *p2);
CHECK(p2->to_string() == canon); // stable
};

check_round_trip("0.0.0.0");
check_round_trip("255.255.255.255");
check_round_trip("192.168.0.1");

// Single-integer form canonicals to dotted-decimal
auto p = ipv4_address::from_string("1");
REQUIRE(p.has_value());
CHECK(p->to_string() == std::string("0.0.0.1"));

// Ordering within IPv4 space
CHECK(ipv4_address{1,2,3,4} < ipv4_address{1,2,3,5});
CHECK(ipv4_address{10,0,0,1} < ipv4_address{10,0,0,2});
}
TEST_SUITE_END();
28 changes: 28 additions & 0 deletions test/ipv4_endpoint_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,32 @@ TEST_CASE("from_string")
ipv4_endpoint{ ipv4_address{ 192, 168, 2, 254 }, 80 });
}

TEST_CASE("round-trip, boundary ports and invalid inputs")
{
// Round-trip canonicalisation
auto check_round_trip = [](const char* s) {
auto p = ipv4_endpoint::from_string(s);
REQUIRE(p.has_value());
auto canon = p->to_string();
auto p2 = ipv4_endpoint::from_string(canon);
REQUIRE(p2.has_value());
CHECK(*p == *p2);
CHECK(p2->to_string() == canon);
};

check_round_trip("192.168.2.254:80");
check_round_trip("0.0.0.0:0");
check_round_trip("255.255.255.255:65535");

// Boundary ports
CHECK(ipv4_endpoint{ ipv4_address{0,0,0,0}, 0 }.to_string() == std::string("0.0.0.0:0"));
CHECK(ipv4_endpoint{ ipv4_address{255,255,255,255}, 65535 }.to_string() == std::string("255.255.255.255:65535"));

// Invalid: whitespace and out-of-range ports
CHECK(ipv4_endpoint::from_string(" 192.168.0.1:80") == std::nullopt);
CHECK(ipv4_endpoint::from_string("192.168.0.1:80 ") == std::nullopt);
CHECK(ipv4_endpoint::from_string("192.168.0.1:65536") == std::nullopt);
CHECK(ipv4_endpoint::from_string("192.168.0.1:-1") == std::nullopt);
}

TEST_SUITE_END();
29 changes: 29 additions & 0 deletions test/ipv6_address_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,35 @@ TEST_CASE("from_string")
ipv6_address(0x20010db885a308d3, 0x13198a2e03707348));
}

TEST_CASE("round-trip and normalization")
{
auto check_round_trip = [](const char* s) {
auto p = ipv6_address::from_string(s);
REQUIRE(p.has_value());
auto canon = p->to_string();
auto p2 = ipv6_address::from_string(canon);
REQUIRE(p2.has_value());
CHECK(*p == *p2);
CHECK(p2->to_string() == canon);
};

// Basic
check_round_trip("::");
check_round_trip("::1");
check_round_trip("FFFF:0000:0000:0000:0000:0000:0000:0001");

// Mixed case input should normalise to lower-case
check_round_trip("AbCd:Ef01::");

// IPv4 interop
check_round_trip("::ffff:192.168.0.1");

// Ordering stability already covered elsewhere; here just ensure canonical contraction is stable
auto p = ipv6_address::from_string("0001:0010:0100:1000::");
REQUIRE(p.has_value());
CHECK(p->to_string() == std::string("1:10:100:1000::"));
}

TEST_CASE("from_string IPv4 interop format")
{
CHECK(
Expand Down
Loading
Loading