diff --git a/.github/compilers.json b/.github/compilers.json index 539f5ecdd..15026a684 100644 --- a/.github/compilers.json +++ b/.github/compilers.json @@ -155,7 +155,7 @@ "latest_cxxstd": "20", "cxx": "g++", "cc": "gcc", - "runs_on": "windows-2022", + "runs_on": "windows-2025", "b2_toolset": "gcc", "generator": "MinGW Makefiles", "shared": false, diff --git a/doc/modules/ROOT/pages/4.guide/4p.unix-sockets.adoc b/doc/modules/ROOT/pages/4.guide/4p.unix-sockets.adoc index 14010fe13..c24c1aee0 100644 --- a/doc/modules/ROOT/pages/4.guide/4p.unix-sockets.adoc +++ b/doc/modules/ROOT/pages/4.guide/4p.unix-sockets.adoc @@ -22,7 +22,7 @@ Code snippets assume: #include #include #include -#include +#include #include #include @@ -123,11 +123,15 @@ capy::task<> client(corosio::io_context& ioc) === Socket Pairs For bidirectional IPC between a parent and child (or two coroutines), -use `make_local_stream_pair()` which calls the `socketpair()` system call: +use `connect_pair()`. On POSIX it uses a single `socketpair()` syscall; +on Windows it performs a private bind/listen/accept/connect rendezvous +on a worker thread. [source,cpp] ---- -auto [s1, s2] = corosio::make_local_stream_pair(ioc); +corosio::local_stream_socket s1(ioc), s2(ioc); +if (auto ec = corosio::connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); // Data written to s1 can be read from s2, and vice versa. co_await s1.write_some(capy::const_buffer("ping", 4)); @@ -138,9 +142,6 @@ auto [ec, n] = co_await s2.read_some( // buf contains "ping" ---- -This is the fastest way to create a connected pair — it uses a single -`socketpair()` syscall with no filesystem paths involved. - == Datagram Sockets Datagram sockets preserve message boundaries. Each `send` delivers exactly @@ -173,7 +174,9 @@ After calling `connect()`, use `send`/`recv` without specifying the peer: [source,cpp] ---- -auto [s1, s2] = corosio::make_local_datagram_pair(ioc); +corosio::local_datagram_socket s1(ioc), s2(ioc); +if (auto ec = corosio::connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); co_await s1.send(capy::const_buffer("msg", 3)); diff --git a/include/boost/corosio.hpp b/include/boost/corosio.hpp index 9a1304508..a61b86fb0 100644 --- a/include/boost/corosio.hpp +++ b/include/boost/corosio.hpp @@ -31,13 +31,19 @@ #include #include +#include #include #include #include #include + +// local_datagram.hpp and local_datagram_socket.hpp are POSIX-only; +// Windows does not support AF_UNIX datagram sockets (SOCK_DGRAM). +#include +#if BOOST_COROSIO_POSIX #include #include -#include +#endif #include #include diff --git a/include/boost/corosio/backend.hpp b/include/boost/corosio/backend.hpp index ea14c1a91..4038f66d1 100644 --- a/include/boost/corosio/backend.hpp +++ b/include/boost/corosio/backend.hpp @@ -241,8 +241,6 @@ class win_local_stream_socket; class win_local_stream_service; class win_local_stream_acceptor; class win_local_stream_acceptor_service; -class win_local_dgram_socket; -class win_local_dgram_service; class win_signal; class win_signals; @@ -278,8 +276,6 @@ struct iocp_t using local_stream_service_type = detail::win_local_stream_service; using local_stream_acceptor_type = detail::win_local_stream_acceptor; using local_stream_acceptor_service_type = detail::win_local_stream_acceptor_service; - using local_datagram_socket_type = detail::win_local_dgram_socket; - using local_datagram_service_type = detail::win_local_dgram_service; /// @} using signal_type = detail::win_signal; diff --git a/include/boost/corosio/detail/local_datagram_service.hpp b/include/boost/corosio/detail/local_datagram_service.hpp index e80d452da..075460e54 100644 --- a/include/boost/corosio/detail/local_datagram_service.hpp +++ b/include/boost/corosio/detail/local_datagram_service.hpp @@ -11,6 +11,10 @@ #define BOOST_COROSIO_DETAIL_LOCAL_DATAGRAM_SERVICE_HPP #include +#include + +#if BOOST_COROSIO_POSIX + #include #include #include @@ -90,4 +94,6 @@ class BOOST_COROSIO_DECL local_datagram_service } // namespace boost::corosio::detail +#endif // BOOST_COROSIO_POSIX + #endif // BOOST_COROSIO_DETAIL_LOCAL_DATAGRAM_SERVICE_HPP diff --git a/include/boost/corosio/local_connect_pair.hpp b/include/boost/corosio/local_connect_pair.hpp new file mode 100644 index 000000000..90ce3acee --- /dev/null +++ b/include/boost/corosio/local_connect_pair.hpp @@ -0,0 +1,80 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_LOCAL_CONNECT_PAIR_HPP +#define BOOST_COROSIO_LOCAL_CONNECT_PAIR_HPP + +#include +#include +#include + +#if BOOST_COROSIO_POSIX +#include +#endif + +#include + +namespace boost::corosio { + +/** Synchronously connect two AF_UNIX stream sockets as a connected pair. + + On POSIX the implementation uses `socketpair(AF_UNIX, SOCK_STREAM)` + and adopts the descriptors via `assign()`. On Windows it performs a + private bind/listen/accept on the calling thread paired with a + `connect()` on a short-lived worker thread; the caller's + `io_context` is never driven, so it may be running on another + thread. + + Either socket may be a `native_local_stream_socket`; the + base reference selects the backend's `assign_socket` through normal + virtual dispatch. + + @par Preconditions + Both sockets must be in the closed state. + + @par Exception Safety + Nothrow. On failure both sockets remain closed and any underlying + resources are released. + + @param a Receives the accepted/first endpoint of the pair. + @param b Receives the connected/second endpoint of the pair. + + @return Empty on success; otherwise the underlying system error. +*/ +BOOST_COROSIO_DECL +std::error_code +connect_pair(local_stream_socket& a, local_stream_socket& b) noexcept; + +#if BOOST_COROSIO_POSIX + +/** Synchronously connect two AF_UNIX datagram sockets as a connected pair. + + POSIX only. Uses `socketpair(AF_UNIX, SOCK_DGRAM)` and adopts the + descriptors via `assign()`. + + @par Preconditions + Both sockets must be in the closed state. + + @par Exception Safety + Nothrow. + + @param a First socket of the pair. + @param b Second socket of the pair. + + @return Empty on success; otherwise the underlying system error. +*/ +BOOST_COROSIO_DECL +std::error_code +connect_pair(local_datagram_socket& a, local_datagram_socket& b) noexcept; + +#endif // BOOST_COROSIO_POSIX + +} // namespace boost::corosio + +#endif diff --git a/include/boost/corosio/local_datagram.hpp b/include/boost/corosio/local_datagram.hpp index 16ba5229b..b75d5c703 100644 --- a/include/boost/corosio/local_datagram.hpp +++ b/include/boost/corosio/local_datagram.hpp @@ -11,6 +11,9 @@ #define BOOST_COROSIO_LOCAL_DATAGRAM_HPP #include +#include + +#if BOOST_COROSIO_POSIX namespace boost::corosio { @@ -25,6 +28,9 @@ class local_datagram_socket; in the compiled library to avoid exposing platform socket headers. + @note Not available on Windows. Windows does not support + AF_UNIX datagram sockets (SOCK_DGRAM). + @see local_datagram_socket */ class BOOST_COROSIO_DECL local_datagram @@ -45,4 +51,6 @@ class BOOST_COROSIO_DECL local_datagram } // namespace boost::corosio +#endif // BOOST_COROSIO_POSIX + #endif // BOOST_COROSIO_LOCAL_DATAGRAM_HPP diff --git a/include/boost/corosio/local_datagram_socket.hpp b/include/boost/corosio/local_datagram_socket.hpp index 7797c3e9e..8bfe16d80 100644 --- a/include/boost/corosio/local_datagram_socket.hpp +++ b/include/boost/corosio/local_datagram_socket.hpp @@ -12,6 +12,9 @@ #include #include + +#if BOOST_COROSIO_POSIX + #include #include #include @@ -56,6 +59,10 @@ namespace boost::corosio { kernel filters incoming datagrams to those from the connected peer. + @note Not available on Windows. Windows does not support + AF_UNIX datagram sockets (SOCK_DGRAM). Attempting to + open this socket on Windows will fail. + @par Cancellation All asynchronous operations support cancellation through `std::stop_token` via the affine protocol, or explicitly @@ -871,4 +878,6 @@ class BOOST_COROSIO_DECL local_datagram_socket : public io_object } // namespace boost::corosio +#endif // BOOST_COROSIO_POSIX + #endif // BOOST_COROSIO_LOCAL_DATAGRAM_SOCKET_HPP diff --git a/include/boost/corosio/local_socket_pair.hpp b/include/boost/corosio/local_socket_pair.hpp deleted file mode 100644 index f9ceb029d..000000000 --- a/include/boost/corosio/local_socket_pair.hpp +++ /dev/null @@ -1,60 +0,0 @@ -// -// Copyright (c) 2026 Michael Vandeberg -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_LOCAL_SOCKET_PAIR_HPP -#define BOOST_COROSIO_LOCAL_SOCKET_PAIR_HPP - -#include -#include - -#if BOOST_COROSIO_POSIX - -#include -#include - -#include - -namespace boost::corosio { - -class io_context; - -/** Create a connected pair of local stream sockets. - - Uses socketpair(AF_UNIX, SOCK_STREAM) to create two - pre-connected sockets. Data written to one can be read - from the other. - - @param ctx The I/O context for the sockets. - - @return A pair of connected local stream sockets. - - @throws std::system_error on failure. -*/ -BOOST_COROSIO_DECL std::pair -make_local_stream_pair(io_context& ctx); - -/** Create a connected pair of local datagram sockets. - - Uses socketpair(AF_UNIX, SOCK_DGRAM) to create two - pre-connected sockets. - - @param ctx The I/O context for the sockets. - - @return A pair of connected local datagram sockets. - - @throws std::system_error on failure. -*/ -BOOST_COROSIO_DECL std::pair -make_local_datagram_pair(io_context& ctx); - -} // namespace boost::corosio - -#endif // BOOST_COROSIO_POSIX - -#endif // BOOST_COROSIO_LOCAL_SOCKET_PAIR_HPP diff --git a/include/boost/corosio/local_stream_socket.hpp b/include/boost/corosio/local_stream_socket.hpp index e9daedcdc..98b385473 100644 --- a/include/boost/corosio/local_stream_socket.hpp +++ b/include/boost/corosio/local_stream_socket.hpp @@ -470,7 +470,7 @@ class BOOST_COROSIO_DECL local_stream_socket : public io_stream The socket must not already be open. The fd is adopted and registered with the platform reactor. Used by - make_local_stream_pair() to wrap socketpair() fds. + connect_pair() to wrap socketpair() fds. @param fd The file descriptor to adopt. Must be a valid, open, non-blocking Unix stream socket. diff --git a/include/boost/corosio/native/detail/iocp/win_local_dgram_service.hpp b/include/boost/corosio/native/detail/iocp/win_local_dgram_service.hpp deleted file mode 100644 index 59a2be664..000000000 --- a/include/boost/corosio/native/detail/iocp/win_local_dgram_service.hpp +++ /dev/null @@ -1,1135 +0,0 @@ -// -// Copyright (c) 2026 Michael Vandeberg -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_LOCAL_DGRAM_SERVICE_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_LOCAL_DGRAM_SERVICE_HPP - -#include - -#if BOOST_COROSIO_HAS_IOCP - -#include -#include - -#include -#include -#include -#include -#include - -#include -#include -#include - -#include - -#include - -namespace boost::corosio::detail { - -/* Map portable message_flags values to native MSG_* constants. */ -inline DWORD -local_dgram_to_native_msg_flags(int flags) noexcept -{ - DWORD native = 0; - if (flags & 1) native |= MSG_PEEK; - if (flags & 2) native |= MSG_OOB; - if (flags & 4) native |= MSG_DONTROUTE; - return native; -} - -/* IOCP local datagram service. - - Inherits from local_datagram_service to enable runtime polymorphism - via use_service(). -*/ -class BOOST_COROSIO_DECL win_local_dgram_service final - : private win_wsa_init - , public local_datagram_service -{ -public: - io_object::implementation* construct() override; - - void destroy(io_object::implementation* p) override; - - void close(io_object::handle& h) override; - - explicit win_local_dgram_service(capy::execution_context& ctx); - - ~win_local_dgram_service(); - - win_local_dgram_service(win_local_dgram_service const&) = delete; - win_local_dgram_service& operator=(win_local_dgram_service const&) = delete; - - void shutdown() override; - - std::error_code open_socket( - local_datagram_socket::implementation& impl, - int family, int type, int protocol) override; - - std::error_code assign_socket( - local_datagram_socket::implementation& impl, - native_handle_type fd) override; - - std::error_code bind_socket( - local_datagram_socket::implementation& impl, - corosio::local_endpoint ep) override; - - void destroy_impl(win_local_dgram_socket& impl); - - void unregister_impl(win_local_dgram_socket_internal& impl); - - std::error_code open_socket_internal( - win_local_dgram_socket_internal& impl, - int family, int type, int protocol); - - void post(overlapped_op* op); - void on_pending(overlapped_op* op) noexcept; - void on_completion(overlapped_op* op, DWORD error, DWORD bytes) noexcept; - void work_started() noexcept; - void work_finished() noexcept; - - /** Return the owning IOCP scheduler. */ - win_scheduler& scheduler() noexcept - { - return sched_; - } - -private: - win_scheduler& sched_; - win_mutex mutex_; - intrusive_list socket_list_; - intrusive_list wrapper_list_; - void* iocp_; -}; - -// ============================================================ -// Operation constructors -// ============================================================ - -inline local_dgram_send_to_op::local_dgram_send_to_op( - win_local_dgram_socket_internal& internal_) noexcept - : overlapped_op(&do_complete) - , internal(internal_) -{ - cancel_func_ = &do_cancel_impl; -} - -inline local_dgram_recv_from_op::local_dgram_recv_from_op( - win_local_dgram_socket_internal& internal_) noexcept - : overlapped_op(&do_complete) - , internal(internal_) -{ - cancel_func_ = &do_cancel_impl; -} - -inline local_dgram_connect_op::local_dgram_connect_op( - win_local_dgram_socket_internal& internal_) noexcept - : overlapped_op(&do_complete) - , internal(internal_) -{ - cancel_func_ = &do_cancel_impl; -} - -inline local_dgram_send_op::local_dgram_send_op( - win_local_dgram_socket_internal& internal_) noexcept - : overlapped_op(&do_complete) - , internal(internal_) -{ - cancel_func_ = &do_cancel_impl; -} - -inline local_dgram_recv_op::local_dgram_recv_op( - win_local_dgram_socket_internal& internal_) noexcept - : overlapped_op(&do_complete) - , internal(internal_) -{ - cancel_func_ = &do_cancel_impl; -} - -inline local_dgram_wait_op::local_dgram_wait_op( - win_local_dgram_socket_internal& internal_) noexcept - : overlapped_op(&do_complete) - , internal(internal_) -{ - cancel_func_ = &do_cancel_impl; -} - -// ============================================================ -// Cancellation functions -// ============================================================ - -inline void -local_dgram_send_to_op::do_cancel_impl(overlapped_op* base) noexcept -{ - auto* op = static_cast(base); - op->cancelled.store(true, std::memory_order_release); - if (op->internal.is_open()) - { - ::CancelIoEx( - reinterpret_cast(op->internal.native_handle()), op); - } -} - -inline void -local_dgram_recv_from_op::do_cancel_impl(overlapped_op* base) noexcept -{ - auto* op = static_cast(base); - op->cancelled.store(true, std::memory_order_release); - if (op->internal.is_open()) - { - ::CancelIoEx( - reinterpret_cast(op->internal.native_handle()), op); - } -} - -inline void -local_dgram_connect_op::do_cancel_impl(overlapped_op* base) noexcept -{ - auto* op = static_cast(base); - op->cancelled.store(true, std::memory_order_release); -} - -inline void -local_dgram_send_op::do_cancel_impl(overlapped_op* base) noexcept -{ - auto* op = static_cast(base); - op->cancelled.store(true, std::memory_order_release); - if (op->internal.is_open()) - { - ::CancelIoEx( - reinterpret_cast(op->internal.native_handle()), op); - } -} - -inline void -local_dgram_recv_op::do_cancel_impl(overlapped_op* base) noexcept -{ - auto* op = static_cast(base); - op->cancelled.store(true, std::memory_order_release); - if (op->internal.is_open()) - { - ::CancelIoEx( - reinterpret_cast(op->internal.native_handle()), op); - } -} - -inline void -local_dgram_wait_op::do_cancel_impl(overlapped_op* base) noexcept -{ - auto* op = static_cast(base); - op->cancelled.store(true, std::memory_order_release); - if (op->internal.is_open()) - { - ::CancelIoEx( - reinterpret_cast(op->internal.native_handle()), op); - } - op->internal.svc_.scheduler().cancel_wait_if_constructed(op); -} - -// ============================================================ -// Completion handlers -// ============================================================ - -inline void -local_dgram_send_to_op::do_complete( - void* owner, - scheduler_op* base, - std::uint32_t /*bytes*/, - std::uint32_t /*error*/) -{ - auto* op = static_cast(base); - if (!owner) - { - op->cleanup_only(); - op->internal_ptr.reset(); - return; - } - auto prevent_premature_destruction = std::move(op->internal_ptr); - op->invoke_handler(); -} - -inline void -local_dgram_recv_from_op::do_complete( - void* owner, - scheduler_op* base, - std::uint32_t /*bytes*/, - std::uint32_t /*error*/) -{ - auto* op = static_cast(base); - if (!owner) - { - op->cleanup_only(); - op->internal_ptr.reset(); - return; - } - - bool success = - (op->dwError == 0 && !op->cancelled.load(std::memory_order_acquire)); - if (success && op->source_out) - { - *op->source_out = from_sockaddr_local( - op->source_storage, static_cast(op->source_len)); - } - - auto prevent_premature_destruction = std::move(op->internal_ptr); - op->invoke_handler(); -} - -inline void -local_dgram_connect_op::do_complete( - void* owner, - scheduler_op* base, - std::uint32_t /*bytes*/, - std::uint32_t /*error*/) -{ - auto* op = static_cast(base); - if (!owner) - { - op->cleanup_only(); - op->internal_ptr.reset(); - return; - } - - bool success = - (op->dwError == 0 && !op->cancelled.load(std::memory_order_acquire)); - if (success) - { - sockaddr_storage local_storage{}; - int local_len = sizeof(local_storage); - if (::getsockname( - op->internal.socket_, - reinterpret_cast(&local_storage), &local_len) == 0) - op->internal.local_endpoint_ = from_sockaddr_local( - local_storage, static_cast(local_len)); - op->internal.remote_endpoint_ = op->target_endpoint; - } - - auto prevent_premature_destruction = std::move(op->internal_ptr); - op->invoke_handler(); -} - -inline void -local_dgram_send_op::do_complete( - void* owner, - scheduler_op* base, - std::uint32_t /*bytes*/, - std::uint32_t /*error*/) -{ - auto* op = static_cast(base); - if (!owner) - { - op->cleanup_only(); - op->internal_ptr.reset(); - return; - } - auto prevent_premature_destruction = std::move(op->internal_ptr); - op->invoke_handler(); -} - -inline void -local_dgram_recv_op::do_complete( - void* owner, - scheduler_op* base, - std::uint32_t /*bytes*/, - std::uint32_t /*error*/) -{ - auto* op = static_cast(base); - if (!owner) - { - op->cleanup_only(); - op->internal_ptr.reset(); - return; - } - auto prevent_premature_destruction = std::move(op->internal_ptr); - op->invoke_handler(); -} - -inline void -local_dgram_wait_op::do_complete( - void* owner, - scheduler_op* base, - std::uint32_t /*bytes*/, - std::uint32_t /*error*/) -{ - auto* op = static_cast(base); - if (!owner) - { - op->cleanup_only(); - op->internal_ptr.reset(); - return; - } - auto prevent_premature_destruction = std::move(op->internal_ptr); - op->invoke_handler(); -} - -// ============================================================ -// win_local_dgram_socket_internal -// ============================================================ - -inline win_local_dgram_socket_internal::win_local_dgram_socket_internal( - win_local_dgram_service& svc) noexcept - : svc_(svc) - , wr_(*this) - , rd_(*this) - , conn_(*this) - , send_wr_(*this) - , recv_rd_(*this) - , wt_(*this) -{ -} - -inline win_local_dgram_socket_internal::~win_local_dgram_socket_internal() -{ - svc_.unregister_impl(*this); -} - -inline SOCKET -win_local_dgram_socket_internal::native_handle() const noexcept -{ - return socket_; -} - -inline corosio::local_endpoint -win_local_dgram_socket_internal::local_endpoint() const noexcept -{ - return local_endpoint_; -} - -inline corosio::local_endpoint -win_local_dgram_socket_internal::remote_endpoint() const noexcept -{ - return remote_endpoint_; -} - -inline bool -win_local_dgram_socket_internal::is_open() const noexcept -{ - return socket_ != INVALID_SOCKET; -} - -inline std::coroutine_handle<> -win_local_dgram_socket_internal::send_to( - std::coroutine_handle<> h, - capy::executor_ref d, - buffer_param param, - corosio::local_endpoint dest, - int flags, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - wr_.internal_ptr = shared_from_this(); - - auto& op = wr_; - op.reset(); - op.h = h; - op.ex = d; - op.ec_out = ec; - op.bytes_out = bytes_out; - op.start(token); - - svc_.work_started(); - - capy::mutable_buffer bufs[local_dgram_send_to_op::max_buffers]; - op.wsabuf_count = - static_cast(param.copy_to(bufs, local_dgram_send_to_op::max_buffers)); - - for (DWORD i = 0; i < op.wsabuf_count; ++i) - { - op.wsabufs[i].buf = static_cast(bufs[i].data()); - op.wsabufs[i].len = static_cast(bufs[i].size()); - } - - op.dest_len = static_cast(to_sockaddr(dest, op.dest_storage)); - - int result = ::WSASendTo( - socket_, op.wsabufs, op.wsabuf_count, nullptr, - local_dgram_to_native_msg_flags(flags), - reinterpret_cast(&op.dest_storage), op.dest_len, &op, - nullptr); - - if (result == SOCKET_ERROR) - { - DWORD err = ::WSAGetLastError(); - if (err != WSA_IO_PENDING) - { - svc_.on_completion(&op, err, 0); - return std::noop_coroutine(); - } - } - - svc_.on_pending(&op); - - if (op.cancelled.load(std::memory_order_acquire)) - ::CancelIoEx(reinterpret_cast(socket_), &op); - - return std::noop_coroutine(); -} - -inline std::coroutine_handle<> -win_local_dgram_socket_internal::recv_from( - std::coroutine_handle<> h, - capy::executor_ref d, - buffer_param param, - corosio::local_endpoint* source, - int flags, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - rd_.internal_ptr = shared_from_this(); - - auto& op = rd_; - op.reset(); - op.h = h; - op.ex = d; - op.ec_out = ec; - op.bytes_out = bytes_out; - op.source_out = source; - op.start(token); - - svc_.work_started(); - - capy::mutable_buffer bufs[local_dgram_recv_from_op::max_buffers]; - op.wsabuf_count = - static_cast(param.copy_to(bufs, local_dgram_recv_from_op::max_buffers)); - - if (op.wsabuf_count == 0 || (op.wsabuf_count == 1 && bufs[0].size() == 0)) - { - op.empty_buffer = true; - svc_.on_completion(&op, 0, 0); - return std::noop_coroutine(); - } - - for (DWORD i = 0; i < op.wsabuf_count; ++i) - { - op.wsabufs[i].buf = static_cast(bufs[i].data()); - op.wsabufs[i].len = static_cast(bufs[i].size()); - } - - op.flags = local_dgram_to_native_msg_flags(flags); - std::memset(&op.source_storage, 0, sizeof(op.source_storage)); - op.source_len = sizeof(op.source_storage); - - int result = ::WSARecvFrom( - socket_, op.wsabufs, op.wsabuf_count, nullptr, &op.flags, - reinterpret_cast(&op.source_storage), &op.source_len, &op, - nullptr); - - if (result == SOCKET_ERROR) - { - DWORD err = ::WSAGetLastError(); - if (err != WSA_IO_PENDING) - { - svc_.on_completion(&op, err, 0); - return std::noop_coroutine(); - } - } - - svc_.on_pending(&op); - - if (op.cancelled.load(std::memory_order_acquire)) - ::CancelIoEx(reinterpret_cast(socket_), &op); - - return std::noop_coroutine(); -} - -// Datagram connect is synchronous on Windows -inline std::coroutine_handle<> -win_local_dgram_socket_internal::connect( - std::coroutine_handle<> h, - capy::executor_ref d, - corosio::local_endpoint ep, - std::stop_token token, - std::error_code* ec) -{ - conn_.internal_ptr = shared_from_this(); - - auto& op = conn_; - op.reset(); - op.h = h; - op.ex = d; - op.ec_out = ec; - op.target_endpoint = ep; - op.start(token); - - svc_.work_started(); - - sockaddr_storage storage{}; - socklen_t addrlen = detail::to_sockaddr(ep, storage); - int result = ::WSAConnect( - socket_, reinterpret_cast(&storage), - static_cast(addrlen), nullptr, nullptr, nullptr, nullptr); - - if (result == SOCKET_ERROR) - svc_.on_completion(&op, ::WSAGetLastError(), 0); - else - svc_.on_completion(&op, 0, 0); - - return std::noop_coroutine(); -} - -inline std::coroutine_handle<> -win_local_dgram_socket_internal::send( - std::coroutine_handle<> h, - capy::executor_ref d, - buffer_param param, - int flags, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - send_wr_.internal_ptr = shared_from_this(); - - auto& op = send_wr_; - op.reset(); - op.h = h; - op.ex = d; - op.ec_out = ec; - op.bytes_out = bytes_out; - op.start(token); - - svc_.work_started(); - - capy::mutable_buffer bufs[local_dgram_send_op::max_buffers]; - op.wsabuf_count = - static_cast(param.copy_to(bufs, local_dgram_send_op::max_buffers)); - - for (DWORD i = 0; i < op.wsabuf_count; ++i) - { - op.wsabufs[i].buf = static_cast(bufs[i].data()); - op.wsabufs[i].len = static_cast(bufs[i].size()); - } - - int result = ::WSASend( - socket_, op.wsabufs, op.wsabuf_count, nullptr, - local_dgram_to_native_msg_flags(flags), &op, nullptr); - - if (result == SOCKET_ERROR) - { - DWORD err = ::WSAGetLastError(); - if (err != WSA_IO_PENDING) - { - svc_.on_completion(&op, err, 0); - return std::noop_coroutine(); - } - } - - svc_.on_pending(&op); - - if (op.cancelled.load(std::memory_order_acquire)) - ::CancelIoEx(reinterpret_cast(socket_), &op); - - return std::noop_coroutine(); -} - -inline std::coroutine_handle<> -win_local_dgram_socket_internal::recv( - std::coroutine_handle<> h, - capy::executor_ref d, - buffer_param param, - int flags, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes_out) -{ - recv_rd_.internal_ptr = shared_from_this(); - - auto& op = recv_rd_; - op.reset(); - op.h = h; - op.ex = d; - op.ec_out = ec; - op.bytes_out = bytes_out; - op.start(token); - - svc_.work_started(); - - capy::mutable_buffer bufs[local_dgram_recv_op::max_buffers]; - op.wsabuf_count = - static_cast(param.copy_to(bufs, local_dgram_recv_op::max_buffers)); - - if (op.wsabuf_count == 0 || (op.wsabuf_count == 1 && bufs[0].size() == 0)) - { - op.empty_buffer = true; - svc_.on_completion(&op, 0, 0); - return std::noop_coroutine(); - } - - for (DWORD i = 0; i < op.wsabuf_count; ++i) - { - op.wsabufs[i].buf = static_cast(bufs[i].data()); - op.wsabufs[i].len = static_cast(bufs[i].size()); - } - - op.flags = local_dgram_to_native_msg_flags(flags); - - int result = ::WSARecv( - socket_, op.wsabufs, op.wsabuf_count, nullptr, &op.flags, &op, nullptr); - - if (result == SOCKET_ERROR) - { - DWORD err = ::WSAGetLastError(); - if (err != WSA_IO_PENDING) - { - svc_.on_completion(&op, err, 0); - return std::noop_coroutine(); - } - } - - svc_.on_pending(&op); - - if (op.cancelled.load(std::memory_order_acquire)) - ::CancelIoEx(reinterpret_cast(socket_), &op); - - return std::noop_coroutine(); -} - -inline std::coroutine_handle<> -win_local_dgram_socket_internal::wait( - std::coroutine_handle<> h, - capy::executor_ref d, - wait_type w, - std::stop_token token, - std::error_code* ec) -{ - wt_.internal_ptr = shared_from_this(); - - auto& op = wt_; - op.reset(); - op.h = h; - op.ex = d; - op.ec_out = ec; - op.bytes_out = nullptr; - op.start(token); - - svc_.work_started(); - - if (w == wait_type::write) - { - svc_.on_completion(&op, 0, 0); - return std::noop_coroutine(); - } - - // Datagram wait_read and wait_error route through the auxiliary - // select reactor. - svc_.scheduler().wait_reactor().register_wait(socket_, w, &op); - return std::noop_coroutine(); -} - -inline void -win_local_dgram_socket_internal::cancel() noexcept -{ - if (socket_ != INVALID_SOCKET) - { - ::CancelIoEx(reinterpret_cast(socket_), nullptr); - } - - wr_.request_cancel(); - rd_.request_cancel(); - conn_.request_cancel(); - send_wr_.request_cancel(); - recv_rd_.request_cancel(); - wt_.request_cancel(); - svc_.scheduler().cancel_wait_if_constructed(&wt_); -} - -inline void -win_local_dgram_socket_internal::close_socket() noexcept -{ - wt_.request_cancel(); - svc_.scheduler().cancel_wait_if_constructed(&wt_); - - if (socket_ != INVALID_SOCKET) - { - ::CancelIoEx(reinterpret_cast(socket_), nullptr); - ::closesocket(socket_); - socket_ = INVALID_SOCKET; - } - - local_endpoint_ = corosio::local_endpoint{}; - remote_endpoint_ = corosio::local_endpoint{}; -} - -// ============================================================ -// win_local_dgram_socket (wrapper) -// ============================================================ - -inline win_local_dgram_socket::win_local_dgram_socket( - std::shared_ptr internal) noexcept - : internal_(std::move(internal)) -{ -} - -inline void -win_local_dgram_socket::close_internal() noexcept -{ - if (internal_) - { - internal_->close_socket(); - internal_.reset(); - } -} - -inline std::coroutine_handle<> -win_local_dgram_socket::send_to( - std::coroutine_handle<> h, capy::executor_ref d, - buffer_param buf, corosio::local_endpoint dest, int flags, - std::stop_token token, std::error_code* ec, std::size_t* bytes) -{ - return internal_->send_to(h, d, buf, dest, flags, token, ec, bytes); -} - -inline std::coroutine_handle<> -win_local_dgram_socket::recv_from( - std::coroutine_handle<> h, capy::executor_ref d, - buffer_param buf, corosio::local_endpoint* source, int flags, - std::stop_token token, std::error_code* ec, std::size_t* bytes) -{ - return internal_->recv_from(h, d, buf, source, flags, token, ec, bytes); -} - -inline std::coroutine_handle<> -win_local_dgram_socket::connect( - std::coroutine_handle<> h, capy::executor_ref d, - corosio::local_endpoint ep, std::stop_token token, std::error_code* ec) -{ - return internal_->connect(h, d, ep, token, ec); -} - -inline std::coroutine_handle<> -win_local_dgram_socket::send( - std::coroutine_handle<> h, capy::executor_ref d, - buffer_param buf, int flags, - std::stop_token token, std::error_code* ec, std::size_t* bytes) -{ - return internal_->send(h, d, buf, flags, token, ec, bytes); -} - -inline std::coroutine_handle<> -win_local_dgram_socket::recv( - std::coroutine_handle<> h, capy::executor_ref d, - buffer_param buf, int flags, - std::stop_token token, std::error_code* ec, std::size_t* bytes) -{ - return internal_->recv(h, d, buf, flags, token, ec, bytes); -} - -inline std::coroutine_handle<> -win_local_dgram_socket::wait( - std::coroutine_handle<> h, - capy::executor_ref d, - wait_type w, - std::stop_token token, - std::error_code* ec) -{ - return internal_->wait(h, d, w, token, ec); -} - -inline std::error_code -win_local_dgram_socket::bind(corosio::local_endpoint ep) noexcept -{ - if (ep.is_abstract()) - return std::make_error_code(std::errc::operation_not_supported); - - SOCKET sock = internal_->socket_; - - sockaddr_storage storage{}; - socklen_t addrlen = detail::to_sockaddr(ep, storage); - if (::bind( - sock, reinterpret_cast(&storage), - static_cast(addrlen)) == SOCKET_ERROR) - return make_err(::WSAGetLastError()); - - internal_->local_endpoint_ = ep; - return {}; -} - -inline std::error_code -win_local_dgram_socket::shutdown( - local_datagram_socket::shutdown_type what) noexcept -{ - int how; - switch (what) - { - case local_datagram_socket::shutdown_receive: - how = SD_RECEIVE; - break; - case local_datagram_socket::shutdown_send: - how = SD_SEND; - break; - case local_datagram_socket::shutdown_both: - how = SD_BOTH; - break; - default: - return make_err(WSAEINVAL); - } - if (::shutdown(internal_->native_handle(), how) != 0) - return make_err(WSAGetLastError()); - return {}; -} - -inline native_handle_type -win_local_dgram_socket::native_handle() const noexcept -{ - return static_cast(internal_->native_handle()); -} - -inline native_handle_type -win_local_dgram_socket::release_socket() noexcept -{ - SOCKET s = internal_->socket_; - if (s != INVALID_SOCKET) - { - internal_->cancel(); - internal_->socket_ = INVALID_SOCKET; - internal_->local_endpoint_ = corosio::local_endpoint{}; - internal_->remote_endpoint_ = corosio::local_endpoint{}; - } - return static_cast(s); -} - -inline std::error_code -win_local_dgram_socket::set_option( - int level, int optname, void const* data, std::size_t size) noexcept -{ - if (::setsockopt( - internal_->native_handle(), level, optname, - reinterpret_cast(data), static_cast(size)) != 0) - return make_err(WSAGetLastError()); - return {}; -} - -inline std::error_code -win_local_dgram_socket::get_option( - int level, int optname, void* data, std::size_t* size) const noexcept -{ - int len = static_cast(*size); - if (::getsockopt( - internal_->native_handle(), level, optname, - reinterpret_cast(data), &len) != 0) - return make_err(WSAGetLastError()); - *size = static_cast(len); - return {}; -} - -inline corosio::local_endpoint -win_local_dgram_socket::local_endpoint() const noexcept -{ - return internal_->local_endpoint(); -} - -inline corosio::local_endpoint -win_local_dgram_socket::remote_endpoint() const noexcept -{ - return internal_->remote_endpoint(); -} - -inline void -win_local_dgram_socket::cancel() noexcept -{ - internal_->cancel(); -} - -inline win_local_dgram_socket_internal* -win_local_dgram_socket::get_internal() const noexcept -{ - return internal_.get(); -} - -// ============================================================ -// win_local_dgram_service -// ============================================================ - -inline win_local_dgram_service::win_local_dgram_service( - capy::execution_context& ctx) - : sched_(ctx.use_service()) - , iocp_(sched_.native_handle()) -{ -} - -inline win_local_dgram_service::~win_local_dgram_service() -{ - for (auto* w = wrapper_list_.pop_front(); w != nullptr; - w = wrapper_list_.pop_front()) - delete w; -} - -inline void -win_local_dgram_service::shutdown() -{ - std::lock_guard lock(mutex_); - - for (auto* impl = socket_list_.pop_front(); impl != nullptr; - impl = socket_list_.pop_front()) - { - impl->close_socket(); - } -} - -inline io_object::implementation* -win_local_dgram_service::construct() -{ - auto internal = std::make_shared(*this); - - { - std::lock_guard lock(mutex_); - socket_list_.push_back(internal.get()); - } - - auto* wrapper = new win_local_dgram_socket(std::move(internal)); - - { - std::lock_guard lock(mutex_); - wrapper_list_.push_back(wrapper); - } - - return wrapper; -} - -inline void -win_local_dgram_service::destroy(io_object::implementation* p) -{ - if (p) - { - auto& wrapper = static_cast(*p); - wrapper.close_internal(); - destroy_impl(wrapper); - } -} - -inline void -win_local_dgram_service::close(io_object::handle& h) -{ - auto& wrapper = static_cast(*h.get()); - wrapper.get_internal()->close_socket(); -} - -inline void -win_local_dgram_service::destroy_impl(win_local_dgram_socket& impl) -{ - { - std::lock_guard lock(mutex_); - wrapper_list_.remove(&impl); - } - delete &impl; -} - -inline void -win_local_dgram_service::unregister_impl( - win_local_dgram_socket_internal& impl) -{ - std::lock_guard lock(mutex_); - socket_list_.remove(&impl); -} - -inline std::error_code -win_local_dgram_service::open_socket( - local_datagram_socket::implementation& impl, - int family, int type, int protocol) -{ - auto& wrapper = static_cast(impl); - return open_socket_internal(*wrapper.get_internal(), family, type, protocol); -} - -inline std::error_code -win_local_dgram_service::assign_socket( - local_datagram_socket::implementation& /*impl*/, native_handle_type /*fd*/) -{ - return std::make_error_code(std::errc::operation_not_supported); -} - -inline std::error_code -win_local_dgram_service::bind_socket( - local_datagram_socket::implementation& impl, - corosio::local_endpoint ep) -{ - // Reject abstract sockets on Windows - if (ep.is_abstract()) - return std::make_error_code(std::errc::operation_not_supported); - - auto& wrapper = static_cast(impl); - auto* internal = wrapper.get_internal(); - SOCKET sock = internal->socket_; - - sockaddr_storage storage{}; - socklen_t addrlen = detail::to_sockaddr(ep, storage); - if (::bind( - sock, reinterpret_cast(&storage), - static_cast(addrlen)) == SOCKET_ERROR) - return make_err(::WSAGetLastError()); - - internal->local_endpoint_ = ep; - return {}; -} - -inline std::error_code -win_local_dgram_service::open_socket_internal( - win_local_dgram_socket_internal& impl, - int family, int type, int protocol) -{ - impl.close_socket(); - - SOCKET sock = - ::WSASocketW(family, type, protocol, nullptr, 0, WSA_FLAG_OVERLAPPED); - - if (sock == INVALID_SOCKET) - return make_err(::WSAGetLastError()); - - HANDLE result = ::CreateIoCompletionPort( - reinterpret_cast(sock), static_cast(iocp_), key_io, 0); - - if (result == nullptr) - { - DWORD dwError = ::GetLastError(); - ::closesocket(sock); - return make_err(dwError); - } - - impl.socket_ = sock; - return {}; -} - -inline void -win_local_dgram_service::post(overlapped_op* op) -{ - sched_.post(op); -} - -inline void -win_local_dgram_service::on_pending(overlapped_op* op) noexcept -{ - sched_.on_pending(op); -} - -inline void -win_local_dgram_service::on_completion( - overlapped_op* op, DWORD error, DWORD bytes) noexcept -{ - sched_.on_completion(op, error, bytes); -} - -inline void -win_local_dgram_service::work_started() noexcept -{ - sched_.work_started(); -} - -inline void -win_local_dgram_service::work_finished() noexcept -{ - sched_.work_finished(); -} - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_IOCP - -#endif // BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_LOCAL_DGRAM_SERVICE_HPP diff --git a/include/boost/corosio/native/detail/iocp/win_local_dgram_socket.hpp b/include/boost/corosio/native/detail/iocp/win_local_dgram_socket.hpp deleted file mode 100644 index e62f29cd9..000000000 --- a/include/boost/corosio/native/detail/iocp/win_local_dgram_socket.hpp +++ /dev/null @@ -1,341 +0,0 @@ -// -// Copyright (c) 2026 Michael Vandeberg -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_LOCAL_DGRAM_SOCKET_HPP -#define BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_LOCAL_DGRAM_SOCKET_HPP - -#include - -#if BOOST_COROSIO_HAS_IOCP - -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -namespace boost::corosio::detail { - -class win_local_dgram_service; -class win_local_dgram_socket_internal; - -/** Send-to operation for local datagram sockets. */ -struct local_dgram_send_to_op : overlapped_op -{ - static constexpr std::size_t max_buffers = 16; - WSABUF wsabufs[max_buffers]; - DWORD wsabuf_count = 0; - sockaddr_storage dest_storage{}; - int dest_len = 0; - win_local_dgram_socket_internal& internal; - std::shared_ptr internal_ptr; - - static void do_complete( - void* owner, - scheduler_op* base, - std::uint32_t bytes, - std::uint32_t error); - static void do_cancel_impl(overlapped_op* op) noexcept; - - explicit local_dgram_send_to_op( - win_local_dgram_socket_internal& internal_) noexcept; -}; - -/** Recv-from operation for local datagram sockets. */ -struct local_dgram_recv_from_op : overlapped_op -{ - static constexpr std::size_t max_buffers = 16; - WSABUF wsabufs[max_buffers]; - DWORD wsabuf_count = 0; - DWORD flags = 0; - sockaddr_storage source_storage{}; - INT source_len = sizeof(sockaddr_storage); - corosio::local_endpoint* source_out = nullptr; - win_local_dgram_socket_internal& internal; - std::shared_ptr internal_ptr; - - static void do_complete( - void* owner, - scheduler_op* base, - std::uint32_t bytes, - std::uint32_t error); - static void do_cancel_impl(overlapped_op* op) noexcept; - - explicit local_dgram_recv_from_op( - win_local_dgram_socket_internal& internal_) noexcept; -}; - -/** Connect operation for connected-mode local datagrams. */ -struct local_dgram_connect_op : overlapped_op -{ - corosio::local_endpoint target_endpoint; - win_local_dgram_socket_internal& internal; - std::shared_ptr internal_ptr; - - static void do_complete( - void* owner, - scheduler_op* base, - std::uint32_t bytes, - std::uint32_t error); - static void do_cancel_impl(overlapped_op* op) noexcept; - - explicit local_dgram_connect_op( - win_local_dgram_socket_internal& internal_) noexcept; -}; - -/** Connected send operation for local datagrams. */ -struct local_dgram_send_op : overlapped_op -{ - static constexpr std::size_t max_buffers = 16; - WSABUF wsabufs[max_buffers]; - DWORD wsabuf_count = 0; - win_local_dgram_socket_internal& internal; - std::shared_ptr internal_ptr; - - static void do_complete( - void* owner, - scheduler_op* base, - std::uint32_t bytes, - std::uint32_t error); - static void do_cancel_impl(overlapped_op* op) noexcept; - - explicit local_dgram_send_op( - win_local_dgram_socket_internal& internal_) noexcept; -}; - -/** Connected recv operation for local datagrams. */ -struct local_dgram_recv_op : overlapped_op -{ - static constexpr std::size_t max_buffers = 16; - WSABUF wsabufs[max_buffers]; - DWORD wsabuf_count = 0; - DWORD flags = 0; - win_local_dgram_socket_internal& internal; - std::shared_ptr internal_ptr; - - static void do_complete( - void* owner, - scheduler_op* base, - std::uint32_t bytes, - std::uint32_t error); - static void do_cancel_impl(overlapped_op* op) noexcept; - - explicit local_dgram_recv_op( - win_local_dgram_socket_internal& internal_) noexcept; -}; - -/** Readiness-wait operation for local datagram sockets. */ -struct local_dgram_wait_op : overlapped_op -{ - win_local_dgram_socket_internal& internal; - std::shared_ptr internal_ptr; - - static void do_complete( - void* owner, - scheduler_op* base, - std::uint32_t bytes, - std::uint32_t error); - static void do_cancel_impl(overlapped_op* op) noexcept; - - explicit local_dgram_wait_op( - win_local_dgram_socket_internal& internal_) noexcept; -}; - -/* Internal local datagram socket state for IOCP. */ -class win_local_dgram_socket_internal - : public intrusive_list::node - , public std::enable_shared_from_this -{ - friend class win_local_dgram_service; - friend class win_local_dgram_socket; - friend struct local_dgram_send_to_op; - friend struct local_dgram_recv_from_op; - friend struct local_dgram_connect_op; - friend struct local_dgram_send_op; - friend struct local_dgram_recv_op; - friend struct local_dgram_wait_op; - - win_local_dgram_service& svc_; - local_dgram_send_to_op wr_; - local_dgram_recv_from_op rd_; - local_dgram_connect_op conn_; - local_dgram_send_op send_wr_; - local_dgram_recv_op recv_rd_; - local_dgram_wait_op wt_; - SOCKET socket_ = INVALID_SOCKET; - -public: - explicit win_local_dgram_socket_internal( - win_local_dgram_service& svc) noexcept; - ~win_local_dgram_socket_internal(); - - std::coroutine_handle<> send_to( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - corosio::local_endpoint, - int flags, - std::stop_token, - std::error_code*, - std::size_t*); - - std::coroutine_handle<> recv_from( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - corosio::local_endpoint*, - int flags, - std::stop_token, - std::error_code*, - std::size_t*); - - std::coroutine_handle<> connect( - std::coroutine_handle<>, - capy::executor_ref, - corosio::local_endpoint, - std::stop_token, - std::error_code*); - - std::coroutine_handle<> send( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - int flags, - std::stop_token, - std::error_code*, - std::size_t*); - - std::coroutine_handle<> recv( - std::coroutine_handle<>, - capy::executor_ref, - buffer_param, - int flags, - std::stop_token, - std::error_code*, - std::size_t*); - - std::coroutine_handle<> wait( - std::coroutine_handle<>, - capy::executor_ref, - wait_type, - std::stop_token, - std::error_code*); - - SOCKET native_handle() const noexcept; - corosio::local_endpoint local_endpoint() const noexcept; - corosio::local_endpoint remote_endpoint() const noexcept; - bool is_open() const noexcept; - void cancel() noexcept; - void close_socket() noexcept; - -private: - corosio::local_endpoint local_endpoint_; - corosio::local_endpoint remote_endpoint_; -}; - -/* Local datagram socket wrapper for IOCP. */ -class win_local_dgram_socket final - : public local_datagram_socket::implementation - , public intrusive_list::node -{ - std::shared_ptr internal_; - -public: - explicit win_local_dgram_socket( - std::shared_ptr internal) noexcept; - - void close_internal() noexcept; - - std::coroutine_handle<> send_to( - std::coroutine_handle<> h, - capy::executor_ref d, - buffer_param buf, - corosio::local_endpoint dest, - int flags, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes) override; - - std::coroutine_handle<> recv_from( - std::coroutine_handle<> h, - capy::executor_ref d, - buffer_param buf, - corosio::local_endpoint* source, - int flags, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes) override; - - std::coroutine_handle<> connect( - std::coroutine_handle<> h, - capy::executor_ref d, - corosio::local_endpoint ep, - std::stop_token token, - std::error_code* ec) override; - - std::coroutine_handle<> send( - std::coroutine_handle<> h, - capy::executor_ref d, - buffer_param buf, - int flags, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes) override; - - std::coroutine_handle<> recv( - std::coroutine_handle<> h, - capy::executor_ref d, - buffer_param buf, - int flags, - std::stop_token token, - std::error_code* ec, - std::size_t* bytes) override; - - std::coroutine_handle<> wait( - std::coroutine_handle<> h, - capy::executor_ref d, - wait_type w, - std::stop_token token, - std::error_code* ec) override; - - std::error_code bind(corosio::local_endpoint ep) noexcept override; - - std::error_code shutdown( - local_datagram_socket::shutdown_type what) noexcept override; - - native_handle_type native_handle() const noexcept override; - - native_handle_type release_socket() noexcept override; - - std::error_code set_option( - int level, - int optname, - void const* data, - std::size_t size) noexcept override; - std::error_code - get_option(int level, int optname, void* data, std::size_t* size) - const noexcept override; - - corosio::local_endpoint local_endpoint() const noexcept override; - corosio::local_endpoint remote_endpoint() const noexcept override; - void cancel() noexcept override; - - win_local_dgram_socket_internal* get_internal() const noexcept; -}; - -} // namespace boost::corosio::detail - -#endif // BOOST_COROSIO_HAS_IOCP - -#endif // BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_LOCAL_DGRAM_SOCKET_HPP diff --git a/include/boost/corosio/native/detail/iocp/win_local_stream_service.hpp b/include/boost/corosio/native/detail/iocp/win_local_stream_service.hpp index 9cc628981..b317670ab 100644 --- a/include/boost/corosio/native/detail/iocp/win_local_stream_service.hpp +++ b/include/boost/corosio/native/detail/iocp/win_local_stream_service.hpp @@ -936,10 +936,26 @@ win_local_stream_service::open_socket( inline std::error_code win_local_stream_service::assign_socket( - local_stream_socket::implementation& /*impl*/, native_handle_type /*fd*/) + local_stream_socket::implementation& impl, native_handle_type fd) { - // socketpair / assign is POSIX-only - return std::make_error_code(std::errc::operation_not_supported); + auto& wrapper = static_cast(impl); + auto& internal = *wrapper.get_internal(); + + internal.close_socket(); + + SOCKET sock = static_cast(fd); + + HANDLE result = ::CreateIoCompletionPort( + reinterpret_cast(sock), static_cast(iocp_), key_io, 0); + + if (result == nullptr) + { + DWORD dwError = ::GetLastError(); + return make_err(dwError); + } + + internal.socket_ = sock; + return {}; } inline std::error_code diff --git a/include/boost/corosio/native/detail/iocp/win_resolver_service.hpp b/include/boost/corosio/native/detail/iocp/win_resolver_service.hpp index c6519c847..00896ed72 100644 --- a/include/boost/corosio/native/detail/iocp/win_resolver_service.hpp +++ b/include/boost/corosio/native/detail/iocp/win_resolver_service.hpp @@ -495,7 +495,7 @@ win_resolver::do_reverse_resolve_work(pool_work_item* w) noexcept inline win_resolver_service::win_resolver_service( capy::execution_context& ctx, scheduler& sched) : sched_(sched) - , pool_(ctx.make_service()) + , pool_(ctx.use_service()) { } diff --git a/include/boost/corosio/native/detail/iocp/win_tcp_acceptor_service.hpp b/include/boost/corosio/native/detail/iocp/win_tcp_acceptor_service.hpp index 3bb98852a..fda667f0a 100644 --- a/include/boost/corosio/native/detail/iocp/win_tcp_acceptor_service.hpp +++ b/include/boost/corosio/native/detail/iocp/win_tcp_acceptor_service.hpp @@ -1373,6 +1373,14 @@ win_tcp_acceptor_internal::accept( } svc_.on_pending(&op); + + // If the stop_token was already cancelled when start() was called, + // the CancelIoEx in the canceller fired before AcceptEx was + // submitted and had no effect. Re-issue now that the I/O is + // pending so the completion posts with ERROR_OPERATION_ABORTED. + if (op.cancelled.load(std::memory_order_acquire)) + ::CancelIoEx(reinterpret_cast(socket_), &op); + return std::noop_coroutine(); } diff --git a/include/boost/corosio/native/detail/iocp/win_wait_reactor.hpp b/include/boost/corosio/native/detail/iocp/win_wait_reactor.hpp index 3617f68a4..89e964cf7 100644 --- a/include/boost/corosio/native/detail/iocp/win_wait_reactor.hpp +++ b/include/boost/corosio/native/detail/iocp/win_wait_reactor.hpp @@ -248,6 +248,16 @@ inline void win_wait_reactor::register_wait( SOCKET fd, wait_type w, overlapped_op* op) { + // If the op was already cancelled (e.g. pre-cancelled stop_token + // fired synchronously before this call), complete immediately + // instead of registering. Otherwise the reactor would park the + // op forever because the earlier cancel_wait() found nothing to + // cancel in registered_. + if (op->cancelled.load(std::memory_order_acquire)) + { + sched_.on_completion(op, 0, 0); + return; + } { std::lock_guard lock(mutex_); pending_register_.push_back(entry{fd, w, op}); diff --git a/include/boost/corosio/native/detail/posix/posix_resolver_service.hpp b/include/boost/corosio/native/detail/posix/posix_resolver_service.hpp index a36b7a7b9..17065f489 100644 --- a/include/boost/corosio/native/detail/posix/posix_resolver_service.hpp +++ b/include/boost/corosio/native/detail/posix/posix_resolver_service.hpp @@ -36,7 +36,7 @@ class BOOST_COROSIO_DECL posix_resolver_service final posix_resolver_service(capy::execution_context& ctx, scheduler& sched) : sched_(&sched) - , pool_(ctx.make_service()) + , pool_(ctx.use_service()) { } diff --git a/include/boost/corosio/native/native_local_datagram_socket.hpp b/include/boost/corosio/native/native_local_datagram_socket.hpp index e603d6161..4ce805774 100644 --- a/include/boost/corosio/native/native_local_datagram_socket.hpp +++ b/include/boost/corosio/native/native_local_datagram_socket.hpp @@ -10,6 +10,10 @@ #ifndef BOOST_COROSIO_NATIVE_NATIVE_LOCAL_DATAGRAM_SOCKET_HPP #define BOOST_COROSIO_NATIVE_NATIVE_LOCAL_DATAGRAM_SOCKET_HPP +#include + +#if BOOST_COROSIO_POSIX + #include #include @@ -25,10 +29,6 @@ #if BOOST_COROSIO_HAS_KQUEUE #include #endif - -#if BOOST_COROSIO_HAS_IOCP -#include -#endif #endif // !BOOST_COROSIO_MRDOCS namespace boost::corosio { @@ -486,4 +486,6 @@ class native_local_datagram_socket : public local_datagram_socket } // namespace boost::corosio +#endif // BOOST_COROSIO_POSIX + #endif // BOOST_COROSIO_NATIVE_NATIVE_LOCAL_DATAGRAM_SOCKET_HPP diff --git a/include/boost/corosio/native/native_socket_option.hpp b/include/boost/corosio/native/native_socket_option.hpp index 97c5a4943..020e4f674 100644 --- a/include/boost/corosio/native/native_socket_option.hpp +++ b/include/boost/corosio/native/native_socket_option.hpp @@ -248,6 +248,86 @@ class integer } }; +/** A boolean socket option with single-byte storage. + + Some BSD-derived kernels (macOS, FreeBSD) require certain IPv4 multicast + options (`IP_MULTICAST_LOOP`) to be set with a one-byte value and return + `EINVAL` for the four-byte form that Linux accepts. This template + provides `unsigned char` storage so the option works on every platform. + + @tparam Level The protocol level. + @tparam Name The option name. +*/ +template +class byte_boolean +{ + unsigned char value_ = 0; + +public: + byte_boolean() = default; + + explicit byte_boolean(bool v) noexcept : value_(v ? 1 : 0) {} + + byte_boolean& operator=(bool v) noexcept + { + value_ = v ? 1 : 0; + return *this; + } + + bool value() const noexcept { return value_ != 0; } + explicit operator bool() const noexcept { return value_ != 0; } + bool operator!() const noexcept { return value_ == 0; } + + static constexpr int level() noexcept { return Level; } + static constexpr int name() noexcept { return Name; } + + void* data() noexcept { return &value_; } + void const* data() const noexcept { return &value_; } + std::size_t size() const noexcept { return sizeof(value_); } + + void resize(std::size_t) noexcept {} +}; + +/** An integer socket option with single-byte storage. + + Same rationale as `byte_boolean`: BSD-derived kernels require + `IP_MULTICAST_TTL` to be set with a one-byte value. Linux accepts + one byte too, so single-byte storage is portable. Values are + truncated to the 0–255 range. + + @tparam Level The protocol level. + @tparam Name The option name. +*/ +template +class byte_integer +{ + unsigned char value_ = 0; + +public: + byte_integer() = default; + + explicit byte_integer(int v) noexcept + : value_(static_cast(v)) + {} + + byte_integer& operator=(int v) noexcept + { + value_ = static_cast(v); + return *this; + } + + int value() const noexcept { return value_; } + + static constexpr int level() noexcept { return Level; } + static constexpr int name() noexcept { return Name; } + + void* data() noexcept { return &value_; } + void const* data() const noexcept { return &value_; } + std::size_t size() const noexcept { return sizeof(value_); } + + void resize(std::size_t) noexcept {} +}; + /** The SO_LINGER socket option (native variant). Controls behavior when closing a socket with unsent data. @@ -375,13 +455,13 @@ using reuse_port = boolean; #endif /// Enable loopback of outgoing multicast on IPv4 (IP_MULTICAST_LOOP). -using multicast_loop_v4 = boolean; +using multicast_loop_v4 = byte_boolean; /// Enable loopback of outgoing multicast on IPv6 (IPV6_MULTICAST_LOOP). using multicast_loop_v6 = boolean; /// Set the multicast TTL for IPv4 (IP_MULTICAST_TTL). -using multicast_hops_v4 = integer; +using multicast_hops_v4 = byte_integer; /// Set the multicast hop limit for IPv6 (IPV6_MULTICAST_HOPS). using multicast_hops_v6 = integer; diff --git a/include/boost/corosio/socket_option.hpp b/include/boost/corosio/socket_option.hpp index 1cb12741a..4e4caaff7 100644 --- a/include/boost/corosio/socket_option.hpp +++ b/include/boost/corosio/socket_option.hpp @@ -172,6 +172,131 @@ class integer_option } }; +/** Base class for concrete boolean socket options with single-byte storage. + + Some BSD-derived kernels (macOS, FreeBSD) require certain IPv4 multicast + options (`IP_MULTICAST_LOOP`) to be set with a one-byte value and return + `EINVAL` for the four-byte form that Linux accepts. This base provides + `unsigned char` storage so the same options work on every platform. +*/ +class byte_boolean_option +{ + unsigned char value_ = 0; + +public: + /// Construct with default value (disabled). + byte_boolean_option() = default; + + /** Construct with an explicit value. + + @param v `true` to enable the option, `false` to disable. + */ + explicit byte_boolean_option(bool v) noexcept : value_(v ? 1 : 0) {} + + /// Assign a new value. + byte_boolean_option& operator=(bool v) noexcept + { + value_ = v ? 1 : 0; + return *this; + } + + /// Return the option value. + bool value() const noexcept + { + return value_ != 0; + } + + /// Return the option value. + explicit operator bool() const noexcept + { + return value_ != 0; + } + + /// Return the negated option value. + bool operator!() const noexcept + { + return value_ == 0; + } + + /// Return a pointer to the underlying storage. + void* data() noexcept + { + return &value_; + } + + /// Return a pointer to the underlying storage. + void const* data() const noexcept + { + return &value_; + } + + /// Return the size of the underlying storage. + std::size_t size() const noexcept + { + return sizeof(value_); + } + + /// Storage is already one byte; no normalization needed. + void resize(std::size_t) noexcept {} +}; + +/** Base class for concrete integer socket options with single-byte storage. + + Same rationale as `byte_boolean_option`: BSD-derived kernels require + `IP_MULTICAST_TTL` to be set with a one-byte value. Linux accepts + one-byte too, so single-byte storage is portable. +*/ +class byte_integer_option +{ + unsigned char value_ = 0; + +public: + /// Construct with default value (zero). + byte_integer_option() = default; + + /** Construct with an explicit value. + + @param v The option value; truncated to one byte. + */ + explicit byte_integer_option(int v) noexcept + : value_(static_cast(v)) + {} + + /// Assign a new value; truncated to one byte. + byte_integer_option& operator=(int v) noexcept + { + value_ = static_cast(v); + return *this; + } + + /// Return the option value. + int value() const noexcept + { + return value_; + } + + /// Return a pointer to the underlying storage. + void* data() noexcept + { + return &value_; + } + + /// Return a pointer to the underlying storage. + void const* data() const noexcept + { + return &value_; + } + + /// Return the size of the underlying storage. + std::size_t size() const noexcept + { + return sizeof(value_); + } + + /// Storage is already one byte; no normalization needed. + void resize(std::size_t) noexcept {} +}; + /** Disable Nagle's algorithm (TCP_NODELAY). @par Example @@ -430,16 +555,19 @@ class BOOST_COROSIO_DECL linger /** Enable loopback of outgoing multicast on IPv4 (IP_MULTICAST_LOOP). + Uses single-byte storage because BSD-derived kernels (macOS, FreeBSD) + reject the four-byte form with `EINVAL`. Linux accepts either size. + @par Example @code sock.set_option( socket_option::multicast_loop_v4( true ) ); @endcode */ -class BOOST_COROSIO_DECL multicast_loop_v4 : public boolean_option +class BOOST_COROSIO_DECL multicast_loop_v4 : public byte_boolean_option { public: - using boolean_option::boolean_option; - using boolean_option::operator=; + using byte_boolean_option::byte_boolean_option; + using byte_boolean_option::operator=; /// Return the protocol level. static int level() noexcept; @@ -470,16 +598,20 @@ class BOOST_COROSIO_DECL multicast_loop_v6 : public boolean_option /** Set the multicast TTL for IPv4 (IP_MULTICAST_TTL). + Uses single-byte storage because BSD-derived kernels (macOS, FreeBSD) + reject the four-byte form with `EINVAL`. Linux accepts either size. + Values are truncated to the 0–255 range. + @par Example @code sock.set_option( socket_option::multicast_hops_v4( 4 ) ); @endcode */ -class BOOST_COROSIO_DECL multicast_hops_v4 : public integer_option +class BOOST_COROSIO_DECL multicast_hops_v4 : public byte_integer_option { public: - using integer_option::integer_option; - using integer_option::operator=; + using byte_integer_option::byte_integer_option; + using byte_integer_option::operator=; /// Return the protocol level. static int level() noexcept; diff --git a/include/boost/corosio/test/local_socket_pair.hpp b/include/boost/corosio/test/local_socket_pair.hpp deleted file mode 100644 index 9793009c7..000000000 --- a/include/boost/corosio/test/local_socket_pair.hpp +++ /dev/null @@ -1,142 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#ifndef BOOST_COROSIO_TEST_LOCAL_SOCKET_PAIR_HPP -#define BOOST_COROSIO_TEST_LOCAL_SOCKET_PAIR_HPP - -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include - -namespace boost::corosio::test { - -/** Create a connected pair of AF_UNIX stream sockets via bind+accept+connect. - - Unlike the library-side @ref make_local_stream_pair (POSIX-only, - socketpair-based), this helper drives the public acceptor API so - it can produce native template wrappers like - `native_local_stream_socket` — the path benchmarks need - to exercise the shadowed read_some/write_some/connect ops. - - @tparam Socket Concrete or native local stream socket type. - @tparam Acceptor Matching acceptor type. - - @param ctx I/O context backing both sockets. - - @return Connected pair `{accepted, connected}`. -*/ -template< - class Socket = local_stream_socket, - class Acceptor = local_stream_acceptor> -std::pair -make_local_stream_pair(io_context& ctx) -{ - namespace fs = std::filesystem; - - static std::random_device rd; - static std::mt19937_64 gen{rd()}; - - std::string path; - for (int attempt = 0; attempt < 16; ++attempt) - { - std::string name = "co_pair_"; - name += std::to_string(gen()); - auto candidate = fs::temp_directory_path() / name; - std::error_code ec; - if (fs::create_directory(candidate, ec)) - { - path = (candidate / "s").string(); - break; - } - } - if (path.empty()) - throw std::runtime_error("make_local_stream_pair: temp path failed"); - - auto ex = ctx.get_executor(); - - Acceptor acc(ctx); - acc.open(); - if (auto ec = acc.bind(local_endpoint(path))) - throw std::runtime_error( - "local_stream_pair bind failed: " + ec.message()); - if (auto ec = acc.listen()) - throw std::runtime_error( - "local_stream_pair listen failed: " + ec.message()); - - Socket s1(ctx); - Socket s2(ctx); - s2.open(); - - std::error_code accept_ec, connect_ec; - bool accept_done = false, connect_done = false; - - capy::run_async(ex)( - [](Acceptor& a, Socket& s, std::error_code& ec_out, - bool& done_out) -> capy::task<> { - auto [ec] = co_await a.accept(s); - ec_out = ec; - done_out = true; - }(acc, s1, accept_ec, accept_done)); - - capy::run_async(ex)( - [](Socket& s, local_endpoint ep, std::error_code& ec_out, - bool& done_out) -> capy::task<> { - auto [ec] = co_await s.connect(ep); - ec_out = ec; - done_out = true; - }(s2, local_endpoint(path), connect_ec, connect_done)); - - ctx.run(); - ctx.restart(); - - // The bind path on disk is no longer needed once accept/connect - // have rendezvoused; remove the file and its parent directory so - // repeated bench invocations don't accumulate cruft under /tmp. - std::error_code rm_ec; - fs::remove(fs::path(path), rm_ec); - fs::remove(fs::path(path).parent_path(), rm_ec); - - if (!accept_done || accept_ec) - { - std::fprintf( - stderr, "local_stream_pair: accept failed (done=%d, ec=%s)\n", - accept_done, accept_ec.message().c_str()); - acc.close(); - throw std::runtime_error("local_stream_pair accept failed"); - } - - if (!connect_done || connect_ec) - { - std::fprintf( - stderr, "local_stream_pair: connect failed (done=%d, ec=%s)\n", - connect_done, connect_ec.message().c_str()); - acc.close(); - s1.close(); - throw std::runtime_error("local_stream_pair connect failed"); - } - - acc.close(); - - return {std::move(s1), std::move(s2)}; -} - -} // namespace boost::corosio::test - -#endif diff --git a/include/boost/corosio/test/temp_path.hpp b/include/boost/corosio/test/temp_path.hpp new file mode 100644 index 000000000..d63980ac0 --- /dev/null +++ b/include/boost/corosio/test/temp_path.hpp @@ -0,0 +1,106 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_TEST_TEMP_PATH_HPP +#define BOOST_COROSIO_TEST_TEMP_PATH_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost::corosio::test { + +/** RAII temp directory holding a path for a Unix-domain socket. + + Creates a unique empty directory under + `std::filesystem::temp_directory_path()` and exposes a path under + it suitable for binding `local_stream_socket` / + `local_datagram_socket`. The destructor removes the directory + (and the bound socket file inside it) recursively, so tests that + throw mid-execution still clean up. + + Naming entropy comes from a process-wide atomic counter mixed with + a one-time `random_device` seed; that's enough to avoid collisions + between parallel test runs without requiring cryptographic + randomness. The constructor retries on collision and throws if it + cannot create a directory in a reasonable number of attempts. + + Platform note: the helper exists so tests don't need to call + `mkdtemp` / `unlink` / `rmdir` directly. On Windows, the path is + a filesystem AF_UNIX path (Windows 10 1803+). +*/ +class temp_socket_dir +{ +public: + temp_socket_dir() + { + namespace fs = std::filesystem; + auto const base = fs::temp_directory_path(); + + // 64 bits of mixed entropy: a random seed established once + // at static init, XORed with a monotonic counter. + static std::uint64_t const seed = [] { + std::random_device rd; + return (static_cast(rd()) << 32) | + static_cast(rd()); + }(); + static std::atomic counter{0}; + + std::error_code ec; + for (int tries = 0; tries < 32; ++tries) + { + auto const n = counter.fetch_add(1, std::memory_order_relaxed); + auto const tag = seed ^ n; + + char buf[32]; + std::snprintf( + buf, sizeof(buf), "corosio_test_%016llx", + static_cast(tag)); + + auto candidate = base / buf; + if (fs::create_directory(candidate, ec)) + { + dir_ = std::move(candidate); + return; + } + } + throw std::runtime_error( + "temp_socket_dir: could not create temp directory"); + } + + ~temp_socket_dir() noexcept + { + if (!dir_.empty()) + { + std::error_code ec; + std::filesystem::remove_all(dir_, ec); + } + } + + temp_socket_dir(temp_socket_dir const&) = delete; + temp_socket_dir& operator=(temp_socket_dir const&) = delete; + + /// Path suitable for binding a local socket. + std::string path() const + { + return (dir_ / "sock").string(); + } + +private: + std::filesystem::path dir_; +}; + +} // namespace boost::corosio::test + +#endif // BOOST_COROSIO_TEST_TEMP_PATH_HPP diff --git a/perf/bench/corosio/local_socket_latency_bench.cpp b/perf/bench/corosio/local_socket_latency_bench.cpp index c8d4a6594..50cd2aac3 100644 --- a/perf/bench/corosio/local_socket_latency_bench.cpp +++ b/perf/bench/corosio/local_socket_latency_bench.cpp @@ -14,10 +14,10 @@ #if BOOST_COROSIO_POSIX #include +#include #include #include #include -#include #include #include #include @@ -26,6 +26,7 @@ #include #include +#include #include #include @@ -78,15 +79,15 @@ template void bench_unix_pingpong_latency(bench::state& state) { - using socket_type = corosio::native_local_stream_socket; - using acceptor_type = corosio::native_local_stream_acceptor; + using socket_type = corosio::native_local_stream_socket; auto message_size = static_cast(state.range(0)); state.counters["message_size"] = static_cast(message_size); corosio::native_io_context ioc; - auto [client, server] = - corosio::test::make_local_stream_pair(ioc); + socket_type client(ioc), server(ioc); + if (auto ec = corosio::connect_pair(client, server)) + throw std::system_error(ec, "connect_pair"); capy::run_async(ioc.get_executor())( unix_pingpong_client_task(client, server, message_size, state)); @@ -110,8 +111,7 @@ template void bench_unix_concurrent_latency(bench::state& state) { - using socket_type = corosio::native_local_stream_socket; - using acceptor_type = corosio::native_local_stream_acceptor; + using socket_type = corosio::native_local_stream_socket; int num_pairs = static_cast(state.range(0)); state.counters["num_pairs"] = num_pairs; @@ -126,9 +126,9 @@ bench_unix_concurrent_latency(bench::state& state) for (int i = 0; i < num_pairs; ++i) { - auto [c, s] = - corosio::test::make_local_stream_pair( - ioc); + socket_type c(ioc), s(ioc); + if (auto ec = corosio::connect_pair(c, s)) + throw std::system_error(ec, "connect_pair"); clients.push_back(std::move(c)); servers.push_back(std::move(s)); } @@ -161,8 +161,7 @@ template void bench_unix_pingpong_latency_lockless(bench::state& state) { - using socket_type = corosio::native_local_stream_socket; - using acceptor_type = corosio::native_local_stream_acceptor; + using socket_type = corosio::native_local_stream_socket; auto message_size = static_cast(state.range(0)); state.counters["message_size"] = static_cast(message_size); @@ -170,8 +169,9 @@ bench_unix_pingpong_latency_lockless(bench::state& state) corosio::io_context_options opts; opts.single_threaded = true; corosio::native_io_context ioc(opts, 1); - auto [client, server] = - corosio::test::make_local_stream_pair(ioc); + socket_type client(ioc), server(ioc); + if (auto ec = corosio::connect_pair(client, server)) + throw std::system_error(ec, "connect_pair"); capy::run_async(ioc.get_executor())( unix_pingpong_client_task(client, server, message_size, state)); @@ -195,8 +195,7 @@ template void bench_unix_concurrent_latency_lockless(bench::state& state) { - using socket_type = corosio::native_local_stream_socket; - using acceptor_type = corosio::native_local_stream_acceptor; + using socket_type = corosio::native_local_stream_socket; int num_pairs = static_cast(state.range(0)); state.counters["num_pairs"] = num_pairs; @@ -213,9 +212,9 @@ bench_unix_concurrent_latency_lockless(bench::state& state) for (int i = 0; i < num_pairs; ++i) { - auto [c, s] = - corosio::test::make_local_stream_pair( - ioc); + socket_type c(ioc), s(ioc); + if (auto ec = corosio::connect_pair(c, s)) + throw std::system_error(ec, "connect_pair"); clients.push_back(std::move(c)); servers.push_back(std::move(s)); } diff --git a/perf/bench/corosio/local_socket_throughput_bench.cpp b/perf/bench/corosio/local_socket_throughput_bench.cpp index 4bacffecc..6c9c0af64 100644 --- a/perf/bench/corosio/local_socket_throughput_bench.cpp +++ b/perf/bench/corosio/local_socket_throughput_bench.cpp @@ -14,16 +14,17 @@ #if BOOST_COROSIO_POSIX #include +#include #include #include #include -#include #include #include #include #include #include +#include #include #include @@ -37,15 +38,15 @@ template void bench_unix_throughput(bench::state& state) { - using socket_type = corosio::native_local_stream_socket; - using acceptor_type = corosio::native_local_stream_acceptor; + using socket_type = corosio::native_local_stream_socket; auto chunk_size = static_cast(state.range(0)); state.counters["chunk_size"] = static_cast(chunk_size); corosio::native_io_context ioc; - auto [writer, reader] = - corosio::test::make_local_stream_pair(ioc); + socket_type writer(ioc), reader(ioc); + if (auto ec = corosio::connect_pair(writer, reader)) + throw std::system_error(ec, "connect_pair"); std::vector write_buf(chunk_size, 'x'); std::vector read_buf(chunk_size); @@ -99,15 +100,15 @@ template void bench_unix_bidirectional_throughput(bench::state& state) { - using socket_type = corosio::native_local_stream_socket; - using acceptor_type = corosio::native_local_stream_acceptor; + using socket_type = corosio::native_local_stream_socket; auto chunk_size = static_cast(state.range(0)); state.counters["chunk_size"] = static_cast(chunk_size); corosio::native_io_context ioc; - auto [sock1, sock2] = - corosio::test::make_local_stream_pair(ioc); + socket_type sock1(ioc), sock2(ioc); + if (auto ec = corosio::connect_pair(sock1, sock2)) + throw std::system_error(ec, "connect_pair"); std::vector buf1(chunk_size, 'a'); std::vector buf2(chunk_size, 'b'); @@ -188,8 +189,7 @@ template void bench_unix_throughput_lockless(bench::state& state) { - using socket_type = corosio::native_local_stream_socket; - using acceptor_type = corosio::native_local_stream_acceptor; + using socket_type = corosio::native_local_stream_socket; auto chunk_size = static_cast(state.range(0)); state.counters["chunk_size"] = static_cast(chunk_size); @@ -197,8 +197,9 @@ bench_unix_throughput_lockless(bench::state& state) corosio::io_context_options opts; opts.single_threaded = true; corosio::native_io_context ioc(opts, 1); - auto [writer, reader] = - corosio::test::make_local_stream_pair(ioc); + socket_type writer(ioc), reader(ioc); + if (auto ec = corosio::connect_pair(writer, reader)) + throw std::system_error(ec, "connect_pair"); std::vector write_buf(chunk_size, 'x'); std::vector read_buf(chunk_size); @@ -252,8 +253,7 @@ template void bench_unix_bidirectional_throughput_lockless(bench::state& state) { - using socket_type = corosio::native_local_stream_socket; - using acceptor_type = corosio::native_local_stream_acceptor; + using socket_type = corosio::native_local_stream_socket; auto chunk_size = static_cast(state.range(0)); state.counters["chunk_size"] = static_cast(chunk_size); @@ -261,8 +261,9 @@ bench_unix_bidirectional_throughput_lockless(bench::state& state) corosio::io_context_options opts; opts.single_threaded = true; corosio::native_io_context ioc(opts, 1); - auto [sock1, sock2] = - corosio::test::make_local_stream_pair(ioc); + socket_type sock1(ioc), sock2(ioc); + if (auto ec = corosio::connect_pair(sock1, sock2)) + throw std::system_error(ec, "connect_pair"); std::vector buf1(chunk_size, 'a'); std::vector buf2(chunk_size, 'b'); diff --git a/src/corosio/src/io_context.cpp b/src/corosio/src/io_context.cpp index 268c9c8a7..bab1f1ade 100644 --- a/src/corosio/src/io_context.cpp +++ b/src/corosio/src/io_context.cpp @@ -33,7 +33,6 @@ #include #include #include -#include #include #include #include @@ -108,7 +107,6 @@ iocp_t::construct(capy::execution_context& ctx, unsigned concurrency_hint) auto& local_svc = ctx.make_service(tcp_svc); ctx.make_service(local_svc); - ctx.make_service(); ctx.make_service(); ctx.make_service(); ctx.make_service(); diff --git a/src/corosio/src/local_connect_pair.cpp b/src/corosio/src/local_connect_pair.cpp new file mode 100644 index 000000000..df56e9bb5 --- /dev/null +++ b/src/corosio/src/local_connect_pair.cpp @@ -0,0 +1,322 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include +#include +#include + +#include + +#if BOOST_COROSIO_POSIX +#include +#include +#include +#include +#elif BOOST_COROSIO_HAS_IOCP +#include + +#include +#include +#include +#include +#include + +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +#ifndef AF_UNIX +#define AF_UNIX 1 +#endif +#endif + +namespace boost::corosio { + +namespace { + +#if BOOST_COROSIO_POSIX + +std::error_code +make_pair_fds(int type, int& a_fd, int& b_fd) noexcept +{ + int fds[2]; + if (::socketpair(AF_UNIX, type, 0, fds) != 0) + return detail::make_err(errno); + + // assign() is documented "adopt-only" and will not mutate the fd; + // set O_NONBLOCK before transferring ownership. + for (int i = 0; i < 2; ++i) + { + int flags = ::fcntl(fds[i], F_GETFL, 0); + if (flags < 0 || ::fcntl(fds[i], F_SETFL, flags | O_NONBLOCK) < 0) + { + auto ec = detail::make_err(errno); + ::close(fds[0]); + ::close(fds[1]); + return ec; + } + } + + a_fd = fds[0]; + b_fd = fds[1]; + return {}; +} + +template +std::error_code +assign_pair(Socket& a, Socket& b, int a_fd, int b_fd) noexcept +{ + try + { + a.assign(a_fd); + } + catch (std::system_error const& e) + { + ::close(a_fd); + ::close(b_fd); + return e.code(); + } + + try + { + b.assign(b_fd); + } + catch (std::system_error const& e) + { + a.close(); + ::close(b_fd); + return e.code(); + } + + return {}; +} + +#elif BOOST_COROSIO_HAS_IOCP + +// Build a unique sub-directory under temp and return the full socket +// path inside it. Empty string on failure. +std::string +pick_pair_path(std::filesystem::path& dir_out) +{ + namespace fs = std::filesystem; + + thread_local std::mt19937_64 gen{std::random_device{}()}; + + for (int attempt = 0; attempt < 16; ++attempt) + { + auto candidate = + fs::temp_directory_path() / + ("co_pair_" + std::to_string(gen())); + std::error_code ec; + if (fs::create_directory(candidate, ec)) + { + dir_out = candidate; + return (candidate / "s").string(); + } + } + return {}; +} + +void +remove_pair_path(std::filesystem::path const& dir, std::string const& path) +{ + std::error_code ec; + std::filesystem::remove(std::filesystem::path(path), ec); + std::filesystem::remove(dir, ec); +} + +// Synchronously rendezvous two AF_UNIX SOCK_STREAM sockets. The +// listener and accept happen on the caller's thread; the connect +// runs on a short-lived worker to avoid a deadlock. The returned +// sockets are created with WSA_FLAG_OVERLAPPED so they can be +// registered with IOCP by assign_socket(). +std::error_code +make_pair_sockets(SOCKET& a_sock, SOCKET& b_sock) noexcept +{ + namespace fs = std::filesystem; + + a_sock = INVALID_SOCKET; + b_sock = INVALID_SOCKET; + + fs::path dir; + std::string path = pick_pair_path(dir); + if (path.empty()) + return detail::make_err(ERROR_PATH_NOT_FOUND); + + SOCKET listen_sock = ::WSASocketW( + AF_UNIX, SOCK_STREAM, 0, nullptr, 0, WSA_FLAG_OVERLAPPED); + if (listen_sock == INVALID_SOCKET) + { + auto ec = detail::make_err(::WSAGetLastError()); + remove_pair_path(dir, path); + return ec; + } + + detail::un_sa_t addr{}; + addr.sun_family = AF_UNIX; + std::strncpy( + addr.sun_path, path.c_str(), sizeof(addr.sun_path) - 1); + int addr_len = static_cast( + offsetof(detail::un_sa_t, sun_path) + path.size() + 1); + + if (::bind( + listen_sock, reinterpret_cast(&addr), addr_len) + == SOCKET_ERROR) + { + auto ec = detail::make_err(::WSAGetLastError()); + ::closesocket(listen_sock); + remove_pair_path(dir, path); + return ec; + } + + if (::listen(listen_sock, 1) == SOCKET_ERROR) + { + auto ec = detail::make_err(::WSAGetLastError()); + ::closesocket(listen_sock); + remove_pair_path(dir, path); + return ec; + } + + SOCKET worker_sock = INVALID_SOCKET; + std::error_code worker_ec; + + std::thread worker([&] { + worker_sock = ::WSASocketW( + AF_UNIX, SOCK_STREAM, 0, nullptr, 0, WSA_FLAG_OVERLAPPED); + if (worker_sock == INVALID_SOCKET) + { + worker_ec = detail::make_err(::WSAGetLastError()); + return; + } + + detail::un_sa_t caddr{}; + caddr.sun_family = AF_UNIX; + std::strncpy( + caddr.sun_path, path.c_str(), sizeof(caddr.sun_path) - 1); + int caddr_len = static_cast( + offsetof(detail::un_sa_t, sun_path) + path.size() + 1); + + if (::connect( + worker_sock, reinterpret_cast(&caddr), caddr_len) + == SOCKET_ERROR) + { + worker_ec = detail::make_err(::WSAGetLastError()); + ::closesocket(worker_sock); + worker_sock = INVALID_SOCKET; + } + }); + + SOCKET accept_sock = ::accept(listen_sock, nullptr, nullptr); + std::error_code accept_ec; + if (accept_sock == INVALID_SOCKET) + accept_ec = detail::make_err(::WSAGetLastError()); + + worker.join(); + + ::closesocket(listen_sock); + remove_pair_path(dir, path); + + if (accept_ec) + { + if (worker_sock != INVALID_SOCKET) + ::closesocket(worker_sock); + return accept_ec; + } + if (worker_ec) + { + ::closesocket(accept_sock); + return worker_ec; + } + + a_sock = accept_sock; + b_sock = worker_sock; + return {}; +} + +std::error_code +assign_pair( + local_stream_socket& a, + local_stream_socket& b, + SOCKET a_sock, + SOCKET b_sock) noexcept +{ + try + { + a.assign(static_cast(a_sock)); + } + catch (std::system_error const& e) + { + ::closesocket(a_sock); + ::closesocket(b_sock); + return e.code(); + } + + try + { + b.assign(static_cast(b_sock)); + } + catch (std::system_error const& e) + { + a.close(); + ::closesocket(b_sock); + return e.code(); + } + + return {}; +} + +#endif + +} // namespace + +std::error_code +connect_pair(local_stream_socket& a, local_stream_socket& b) noexcept +{ + if (a.is_open() || b.is_open()) + return detail::make_err( +#if BOOST_COROSIO_POSIX + EISCONN +#else + WSAEISCONN +#endif + ); + +#if BOOST_COROSIO_POSIX + int a_fd = -1, b_fd = -1; + if (auto ec = make_pair_fds(SOCK_STREAM, a_fd, b_fd)) + return ec; + return assign_pair(a, b, a_fd, b_fd); +#elif BOOST_COROSIO_HAS_IOCP + SOCKET a_sock = INVALID_SOCKET, b_sock = INVALID_SOCKET; + if (auto ec = make_pair_sockets(a_sock, b_sock)) + return ec; + return assign_pair(a, b, a_sock, b_sock); +#else + return detail::make_err(ENOSYS); +#endif +} + +#if BOOST_COROSIO_POSIX + +std::error_code +connect_pair(local_datagram_socket& a, local_datagram_socket& b) noexcept +{ + if (a.is_open() || b.is_open()) + return detail::make_err(EISCONN); + + int a_fd = -1, b_fd = -1; + if (auto ec = make_pair_fds(SOCK_DGRAM, a_fd, b_fd)) + return ec; + return assign_pair(a, b, a_fd, b_fd); +} + +#endif + +} // namespace boost::corosio diff --git a/src/corosio/src/local_datagram.cpp b/src/corosio/src/local_datagram.cpp index 31b841550..8f305e8de 100644 --- a/src/corosio/src/local_datagram.cpp +++ b/src/corosio/src/local_datagram.cpp @@ -11,35 +11,22 @@ #include #if BOOST_COROSIO_POSIX + #include #include -#elif BOOST_COROSIO_HAS_IOCP -#include -#ifndef AF_UNIX -#define AF_UNIX 1 -#endif -#endif namespace boost::corosio { int local_datagram::family() noexcept { -#if BOOST_COROSIO_POSIX || BOOST_COROSIO_HAS_IOCP return AF_UNIX; -#else - return 0; -#endif } int local_datagram::type() noexcept { -#if BOOST_COROSIO_POSIX || BOOST_COROSIO_HAS_IOCP return SOCK_DGRAM; -#else - return 0; -#endif } int @@ -49,3 +36,5 @@ local_datagram::protocol() noexcept } } // namespace boost::corosio + +#endif // BOOST_COROSIO_POSIX diff --git a/src/corosio/src/local_datagram_socket.cpp b/src/corosio/src/local_datagram_socket.cpp index e68758f46..ea2be7d71 100644 --- a/src/corosio/src/local_datagram_socket.cpp +++ b/src/corosio/src/local_datagram_socket.cpp @@ -9,17 +9,13 @@ #include -#if BOOST_COROSIO_POSIX || BOOST_COROSIO_HAS_IOCP +#if BOOST_COROSIO_POSIX #include #include #include -#if BOOST_COROSIO_POSIX #include -#elif BOOST_COROSIO_HAS_IOCP -#include -#endif namespace boost::corosio { @@ -113,11 +109,7 @@ native_handle_type local_datagram_socket::native_handle() const noexcept { if (!is_open()) -#if BOOST_COROSIO_HAS_IOCP - return ~native_handle_type(0); -#else return -1; -#endif return get().native_handle(); } @@ -134,22 +126,12 @@ local_datagram_socket::available() const { if (!is_open()) detail::throw_logic_error("available: socket not open"); -#if BOOST_COROSIO_HAS_IOCP - u_long value = 0; - if (::ioctlsocket( - static_cast(native_handle()), FIONREAD, &value) != 0) - detail::throw_system_error( - std::error_code(::WSAGetLastError(), std::system_category()), - "local_datagram_socket::available"); - return static_cast(value); -#else int value = 0; if (::ioctl(native_handle(), FIONREAD, &value) < 0) detail::throw_system_error( std::error_code(errno, std::system_category()), "local_datagram_socket::available"); return static_cast(value); -#endif } local_endpoint @@ -170,4 +152,4 @@ local_datagram_socket::remote_endpoint() const noexcept } // namespace boost::corosio -#endif // BOOST_COROSIO_POSIX || BOOST_COROSIO_HAS_IOCP +#endif // BOOST_COROSIO_POSIX diff --git a/src/corosio/src/local_socket_pair.cpp b/src/corosio/src/local_socket_pair.cpp deleted file mode 100644 index 9342db5b4..000000000 --- a/src/corosio/src/local_socket_pair.cpp +++ /dev/null @@ -1,134 +0,0 @@ -// -// Copyright (c) 2026 Michael Vandeberg -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -#include -#include -#include - -#if BOOST_COROSIO_POSIX - -#include -#include -#include - -#include -#include -#include - -namespace boost::corosio { - -namespace { - -#ifndef SOCK_NONBLOCK -void -make_nonblock_cloexec(int fd) -{ - int fl = ::fcntl(fd, F_GETFL, 0); - if (fl < 0) - throw std::system_error( - std::error_code(errno, std::system_category()), - "fcntl(F_GETFL)"); - if (::fcntl(fd, F_SETFL, fl | O_NONBLOCK) < 0) - throw std::system_error( - std::error_code(errno, std::system_category()), - "fcntl(F_SETFL)"); - if (::fcntl(fd, F_SETFD, FD_CLOEXEC) < 0) - throw std::system_error( - std::error_code(errno, std::system_category()), - "fcntl(F_SETFD)"); -} -#endif - -void -create_pair(int type, int fds[2]) -{ - int flags = type; -#ifdef SOCK_NONBLOCK - flags |= SOCK_NONBLOCK | SOCK_CLOEXEC; -#endif - if (::socketpair(AF_UNIX, flags, 0, fds) != 0) - throw std::system_error( - std::error_code(errno, std::system_category()), - "socketpair"); -#ifndef SOCK_NONBLOCK - try - { - make_nonblock_cloexec(fds[0]); - make_nonblock_cloexec(fds[1]); - } - catch (...) - { - ::close(fds[0]); - ::close(fds[1]); - throw; - } -#endif -} - -} // namespace - -std::pair -make_local_stream_pair(io_context& ctx) -{ - int fds[2]; - create_pair(SOCK_STREAM, fds); - - try - { - local_stream_socket s1(ctx); - local_stream_socket s2(ctx); - - s1.assign(fds[0]); - fds[0] = -1; - s2.assign(fds[1]); - fds[1] = -1; - - return {std::move(s1), std::move(s2)}; - } - catch (...) - { - if (fds[0] >= 0) - ::close(fds[0]); - if (fds[1] >= 0) - ::close(fds[1]); - throw; - } -} - -std::pair -make_local_datagram_pair(io_context& ctx) -{ - int fds[2]; - create_pair(SOCK_DGRAM, fds); - - try - { - local_datagram_socket s1(ctx); - local_datagram_socket s2(ctx); - - s1.assign(fds[0]); - fds[0] = -1; - s2.assign(fds[1]); - fds[1] = -1; - - return {std::move(s1), std::move(s2)}; - } - catch (...) - { - if (fds[0] >= 0) - ::close(fds[0]); - if (fds[1] >= 0) - ::close(fds[1]); - throw; - } -} - -} // namespace boost::corosio - -#endif // BOOST_COROSIO_POSIX diff --git a/test/unit/Jamfile b/test/unit/Jamfile index f16ab3cbd..3425f343a 100644 --- a/test/unit/Jamfile +++ b/test/unit/Jamfile @@ -7,6 +7,7 @@ # Official repository: https://github.com/cppalliance/corosio # +import os ; import testing ; project boost/corosio/test/unit @@ -17,14 +18,22 @@ project boost/corosio/test/unit ../../../capy/extra/test_suite . ../.. + windows:_WIN32_WINNT=0x0602 ; -# Non-TLS tests -for local f in [ glob *.cpp : openssl_stream.cpp wolfssl_stream.cpp cross_ssl_stream.cpp tls_stream.cpp tls_stream_stress.cpp ] [ glob test/*.cpp ] +# Non-TLS tests (recurses into test/, native/, etc.) +for local f in [ glob-tree-ex . : *.cpp : openssl_stream.cpp wolfssl_stream.cpp cross_ssl_stream.cpp tls_stream.cpp tls_stream_stress.cpp iocp_shutdown.cpp ] { run $(f) ; } +# IOCP-specific native tests (Windows host only; skip on other hosts to +# avoid triggering a cross-compile attempt). +if [ os.name ] = NT +{ + run native/iocp/iocp_shutdown.cpp ; +} + # OpenSSL tests - always link against boost_corosio_openssl # The library itself has no if OpenSSL is not found # The test has static_assert to fail compilation if OpenSSL define is missing diff --git a/test/unit/io_context.cpp b/test/unit/io_context.cpp index cb4ff5586..5a8293212 100644 --- a/test/unit/io_context.cpp +++ b/test/unit/io_context.cpp @@ -10,6 +10,8 @@ // Test that header file is self-contained. #include +#include + #include #include #include @@ -253,6 +255,49 @@ struct io_context_test } } + void testConstructionWithOptions() + { + // Tune reactor budgets (POSIX) and IOCP gqcs timeout so the + // option-applying constructor path exercises non-default values. + io_context_options opts; + opts.max_events_per_poll = 256; + opts.inline_budget_initial = 4; + opts.inline_budget_max = 32; + opts.unassisted_budget = 8; + opts.gqcs_timeout_ms = 250; + + io_context ioc(opts, 2); + BOOST_TEST(!ioc.stopped()); + + // Single-arg constructor with options + default concurrency + io_context ioc2(opts); + BOOST_TEST(!ioc2.stopped()); + } + + void testConstructionWithThreadPoolSize() + { + io_context_options opts; + opts.thread_pool_size = 4; + io_context ioc(opts, 2); + BOOST_TEST(!ioc.stopped()); + } + + void testConstructionSingleThreaded() + { + // concurrency_hint == 1 enables single-threaded mode automatically. + io_context_options opts; + opts.single_threaded = true; + io_context ioc(opts, 1); + BOOST_TEST(!ioc.stopped()); + + int counter = 0; + auto ex = ioc.get_executor(); + post_coro(ex, make_coro(counter)); + std::size_t n = ioc.run(); + BOOST_TEST(n == 1); + BOOST_TEST(counter == 1); + } + void testGetExecutor() { io_context ioc; @@ -642,9 +687,95 @@ struct io_context_test BOOST_TEST_EQ(destroyed, 3); } + // Exercises continuation_op::destroy() — invoked when shutdown drains + // queued continuation_op posts. The tagged-post path through + // executor::post(capy::continuation&) routes to scheduler::post(scheduler_op*) + // which enqueues without heap allocation; on shutdown the queue is drained + // and destroy() must release each continuation's coroutine frame. + void testContinuationOpDestroyOnShutdown() + { + int destroyed = 0; + + // Allocate the continuation_ops outside the io_context scope so the + // ops outlive the scheduler that points at them. + detail::continuation_op op1; + detail::continuation_op op2; + op1.cont.h = make_destroy_coro(destroyed); + op2.cont.h = make_destroy_coro(destroyed); + + { + io_context ioc; + auto ex = ioc.get_executor(); + + ex.post(op1.cont); + ex.post(op2.cont); + + // io_context destructor drains scheduler queue and calls + // continuation_op::destroy() on each. + } + + BOOST_TEST_EQ(destroyed, 2); + } + + // Exercises the `rel_time > 1s` clamp branch in run_one_until. + // With no work and a deadline >1s in the future, the inner loop + // iterates with rel_time clamped to 1s before returning 0. + void testRunOneUntilLongDeadlineNoWork() + { + io_context ioc; + + // Deadline >1s but tiny outstanding work so wait_one is not + // entered: scheduler is empty, wait_one immediately stops and + // returns 0. The outer run_one_until loop still enters with + // rel_time > 1s, hitting the clamp branch. + auto deadline = + std::chrono::steady_clock::now() + std::chrono::seconds(2); + std::size_t n = ioc.run_one_until(deadline); + BOOST_TEST(n == 0); + BOOST_TEST(ioc.stopped()); + } + + // MT-mode test that exercises conditionally_enabled_event::wait_for() + // and cross-thread notify_one(). A work guard keeps run_for inside its + // wait_for; main posts 8 handlers from a different thread, each of + // which triggers notify_one. Main polls counter until all handlers + // have run, then releases the work guard so run_for exits. The + // assertion depends only on the completed work, not on wall-clock + // timing. + void testMultithreadedNotifyAndWaitFor() + { + io_context ioc; // default hint => MT mode + auto ex = ioc.get_executor(); + std::atomic counter{0}; + + // Work guard prevents run_for from short-circuiting on an + // empty queue before main posts any work. + ex.on_work_started(); + + std::thread runner([&]() { + // 5s ceiling is a safety net only; we release the guard + // below as soon as work is drained. + (void)ioc.run_for(std::chrono::seconds(5)); + }); + + for (int i = 0; i < 8; ++i) + post_coro(ex, make_atomic_coro(counter)); + + while (counter.load() < 8) + std::this_thread::yield(); + + ex.on_work_finished(); + runner.join(); + + BOOST_TEST_EQ(counter.load(), 8); + } + void run() { testConstruction(); + testConstructionWithOptions(); + testConstructionWithThreadPoolSize(); + testConstructionSingleThreaded(); testGetExecutor(); testRun(); testRunOne(); @@ -653,14 +784,17 @@ struct io_context_test testStopAndRestart(); testRunOneFor(); testRunOneUntil(); + testRunOneUntilLongDeadlineNoWork(); testRunFor(); testRunForWithOutstandingWork(); testRunOneForWithOutstandingWork(); testExecutorRunningInThisThread(); testMultithreaded(); testMultithreadedStress(); + testMultithreadedNotifyAndWaitFor(); testWhenAllSetEvent(); testShutdownDestroysPostedCoroutineFrames(); + testContinuationOpDestroyOnShutdown(); } }; @@ -686,9 +820,24 @@ struct io_context_shutdown_test BOOST_TEST_EQ(destroyed, 3); } + void testConstructionWithBackendAndOptions() + { + // Exercises the templated io_context(Backend, options, hint) + // constructor that runs apply_options_pre_/_post_. + io_context_options opts; + opts.max_events_per_poll = 64; + opts.inline_budget_initial = 4; + opts.inline_budget_max = 16; + opts.unassisted_budget = 4; + + io_context ioc(Backend, opts, 2); + BOOST_TEST(!ioc.stopped()); + } + void run() { testShutdownDestroysPostedCoroutineFrames(); + testConstructionWithBackendAndOptions(); } }; diff --git a/test/unit/ipv6_address.cpp b/test/unit/ipv6_address.cpp index fec605415..f45e0e10d 100644 --- a/test/unit/ipv6_address.cpp +++ b/test/unit/ipv6_address.cpp @@ -147,6 +147,47 @@ struct ipv6_address_test BOOST_TEST_EQ(sv, "::1"); } + void testToBufferTooSmallThrows() + { + // to_buffer must throw length_error when the buffer is smaller + // than max_str_len, even if the formatted address would fit. + char small[4]; + BOOST_TEST_THROWS( + ipv6_address::loopback().to_buffer(small, sizeof(small)), + std::length_error); + } + + void testToStringHexWidths() + { + // Exercise each print_hex width branch (4, 3, 2, 1 hex digit). + // 4 digits: 0xabcd, 3 digits: 0x0bcd, 2 digits: 0x00bc, 1 digit: 0x000b. + ipv6_address::bytes_type b{ + {0xab, 0xcd, 0x0b, 0xcd, 0x00, 0xbc, 0x00, 0x0b, + 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}}; + ipv6_address a(b); + BOOST_TEST_EQ(a.to_string(), "abcd:bcd:bc:b:1234:5678:9abc:def0"); + } + + void testParseEndsWithDoubleColon() + { + // "1::" — '::' at the end requires the "ends in ::" hex break path. + ipv6_address addr; + auto ec = parse_ipv6_address("1::", addr); + BOOST_TEST(!ec); + BOOST_TEST_EQ(addr.to_string(), "1::"); + } + + void testParseInvalidIPv4Suffix() + { + ipv6_address addr; + // "::1.2.3" — IPv4 portion incomplete. + BOOST_TEST(parse_ipv6_address("::1.2.3", addr)); + // "::g.0.0.0" — non-numeric hex. + BOOST_TEST(parse_ipv6_address("::g.0.0.0", addr)); + // "1:2:3:4:5:6.7.8.9" — IPv4 with no '::' but not enough h16 groups. + BOOST_TEST(parse_ipv6_address("1:2:3:4:5:6.7.8.9", addr)); + } + void testPredicates() { // Unspecified @@ -202,8 +243,12 @@ struct ipv6_address_test { testConstruction(); testParse(); + testParseEndsWithDoubleColon(); + testParseInvalidIPv4Suffix(); testToString(); + testToStringHexWidths(); testToBuffer(); + testToBufferTooSmallThrows(); testPredicates(); testComparison(); testOstream(); diff --git a/test/unit/local_connect_pair.cpp b/test/unit/local_connect_pair.cpp new file mode 100644 index 000000000..02ad27358 --- /dev/null +++ b/test/unit/local_connect_pair.cpp @@ -0,0 +1,151 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +// Test that header file is self-contained. +#include + +#include +#include +#include + +#if BOOST_COROSIO_POSIX +#include +#endif + +#include +#include +#include + +#include +#include + +#include "context.hpp" +#include "test_suite.hpp" + +namespace boost::corosio { + +template +struct local_connect_pair_test +{ + void testStreamPairExchangesData() + { + io_context ioc(Backend); + local_stream_socket a(ioc), b(ioc); + + BOOST_TEST(!connect_pair(a, b)); + BOOST_TEST(a.is_open()); + BOOST_TEST(b.is_open()); + + auto ex = ioc.get_executor(); + + char const msg[] = "ping"; + char buf[16] = {}; + + std::error_code wec, rec; + std::size_t wn = 0, rn = 0; + + capy::run_async(ex)( + [](local_stream_socket& s, char const* d, std::size_t len, + std::error_code& ec_out, std::size_t& n_out) -> capy::task<> { + auto [ec, n] = + co_await s.write_some(capy::const_buffer(d, len)); + ec_out = ec; + n_out = n; + }(a, msg, std::strlen(msg), wec, wn)); + + capy::run_async(ex)( + [](local_stream_socket& s, char* d, std::size_t len, + std::error_code& ec_out, std::size_t& n_out) -> capy::task<> { + auto [ec, n] = + co_await s.read_some(capy::mutable_buffer(d, len)); + ec_out = ec; + n_out = n; + }(b, buf, sizeof(buf), rec, rn)); + + ioc.run(); + + BOOST_TEST(!wec); + BOOST_TEST(!rec); + BOOST_TEST_EQ(wn, std::strlen(msg)); + BOOST_TEST_EQ(rn, std::strlen(msg)); + BOOST_TEST_EQ(std::strncmp(buf, msg, std::strlen(msg)), 0); + } + + void testStreamPairRejectsOpenSocket() + { + io_context ioc(Backend); + local_stream_socket a(ioc), b(ioc); + a.open(); + // a is open; connect_pair must refuse and leave both sockets + // in their original state (a open, b closed). + auto ec = connect_pair(a, b); + BOOST_TEST(static_cast(ec)); + BOOST_TEST(a.is_open()); + BOOST_TEST(!b.is_open()); + } + +#if BOOST_COROSIO_POSIX + void testDatagramPairExchangesData() + { + io_context ioc(Backend); + local_datagram_socket a(ioc), b(ioc); + + BOOST_TEST(!connect_pair(a, b)); + BOOST_TEST(a.is_open()); + BOOST_TEST(b.is_open()); + + auto ex = ioc.get_executor(); + + char const msg[] = "dgram"; + char buf[16] = {}; + + std::error_code sec, rec; + std::size_t sn = 0, rn = 0; + + capy::run_async(ex)( + [](local_datagram_socket& s, char const* d, std::size_t len, + std::error_code& ec_out, std::size_t& n_out) -> capy::task<> { + auto [ec, n] = + co_await s.send(capy::const_buffer(d, len)); + ec_out = ec; + n_out = n; + }(a, msg, std::strlen(msg), sec, sn)); + + capy::run_async(ex)( + [](local_datagram_socket& s, char* d, std::size_t len, + std::error_code& ec_out, std::size_t& n_out) -> capy::task<> { + auto [ec, n] = + co_await s.recv(capy::mutable_buffer(d, len)); + ec_out = ec; + n_out = n; + }(b, buf, sizeof(buf), rec, rn)); + + ioc.run(); + + BOOST_TEST(!sec); + BOOST_TEST(!rec); + BOOST_TEST_EQ(sn, std::strlen(msg)); + BOOST_TEST_EQ(rn, std::strlen(msg)); + BOOST_TEST_EQ(std::strncmp(buf, msg, std::strlen(msg)), 0); + } +#endif + + void run() + { + testStreamPairExchangesData(); + testStreamPairRejectsOpenSocket(); +#if BOOST_COROSIO_POSIX + testDatagramPairExchangesData(); +#endif + } +}; + +COROSIO_BACKEND_TESTS(local_connect_pair_test, "boost.corosio.local_connect_pair") + +} // namespace boost::corosio diff --git a/test/unit/local_datagram_socket.cpp b/test/unit/local_datagram_socket.cpp index 1f8076867..ae8b493cd 100644 --- a/test/unit/local_datagram_socket.cpp +++ b/test/unit/local_datagram_socket.cpp @@ -18,24 +18,28 @@ // Keep the entire suite POSIX-gated until Windows kernel support lands. #if BOOST_COROSIO_POSIX +#include #include -#include +#include +#include #include +#include #include #include +#include #include +#include #include +#include + +#include #include "context.hpp" -#include "local_temp.hpp" #include "test_suite.hpp" namespace boost::corosio { -using test::make_temp_socket_path; -using test::cleanup_temp_socket; - template struct local_datagram_socket_test { @@ -73,7 +77,9 @@ struct local_datagram_socket_test void testSendRecvConnected() { io_context ioc(Backend); - auto [s1, s2] = make_local_datagram_pair(ioc); + local_datagram_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); auto ex = ioc.get_executor(); @@ -123,11 +129,10 @@ struct local_datagram_socket_test local_datagram_socket sock(ioc); sock.open(); - auto path = make_temp_socket_path(); + test::temp_socket_dir tmp; + auto path = tmp.path(); auto ec = sock.bind(local_endpoint(path)); BOOST_TEST_EQ(!ec, true); - - cleanup_temp_socket(path); } void testSendToRecvFrom() @@ -135,8 +140,10 @@ struct local_datagram_socket_test io_context ioc(Backend); auto ex = ioc.get_executor(); - auto path1 = make_temp_socket_path(); - auto path2 = make_temp_socket_path(); + test::temp_socket_dir tmp1; + test::temp_socket_dir tmp2; + auto path1 = tmp1.path(); + auto path2 = tmp2.path(); local_datagram_socket s1(ioc); local_datagram_socket s2(ioc); @@ -193,9 +200,6 @@ struct local_datagram_socket_test // Source endpoint should be the sender's bound path BOOST_TEST_EQ(source.path(), path1); - - cleanup_temp_socket(path1); - cleanup_temp_socket(path2); } void testBindFailure() @@ -212,7 +216,9 @@ struct local_datagram_socket_test void testDatagramBoundary() { io_context ioc(Backend); - auto [s1, s2] = make_local_datagram_pair(ioc); + local_datagram_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); auto ex = ioc.get_executor(); // Send two messages of different sizes, verify they @@ -357,7 +363,9 @@ struct local_datagram_socket_test void testRecvPeek() { io_context ioc(Backend); - auto [s1, s2] = make_local_datagram_pair(ioc); + local_datagram_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); auto ex = ioc.get_executor(); // Send a message, peek at it, then consume it. @@ -422,8 +430,10 @@ struct local_datagram_socket_test io_context ioc(Backend); auto ex = ioc.get_executor(); - auto path1 = make_temp_socket_path(); - auto path2 = make_temp_socket_path(); + test::temp_socket_dir tmp1; + test::temp_socket_dir tmp2; + auto path1 = tmp1.path(); + auto path2 = tmp2.path(); local_datagram_socket s1(ioc); local_datagram_socket s2(ioc); @@ -501,9 +511,280 @@ struct local_datagram_socket_test // Source should be the sender's bound path BOOST_TEST_EQ(src1.path(), path1); BOOST_TEST_EQ(src2.path(), path1); + } + + void testMoveAssign() + { + io_context ioc(Backend); + local_datagram_socket s1(ioc); + local_datagram_socket s2(ioc); + s1.open(); + BOOST_TEST_EQ(s1.is_open(), true); + + s2 = std::move(s1); + BOOST_TEST_EQ(s2.is_open(), true); + BOOST_TEST_EQ(s1.is_open(), false); + } + + void testCancelOnClosed() + { + io_context ioc(Backend); + local_datagram_socket sock(ioc); + + // cancel() on a closed socket is a no-op (early return). + sock.cancel(); + BOOST_TEST_EQ(sock.is_open(), false); + } + + void testNativeHandleClosed() + { + io_context ioc(Backend); + local_datagram_socket sock(ioc); + + BOOST_TEST_EQ(sock.native_handle() < 0, true); + + sock.open(); + BOOST_TEST(sock.native_handle() >= 0); + } + + void testEndpointsClosed() + { + io_context ioc(Backend); + local_datagram_socket sock(ioc); + + BOOST_TEST_EQ(sock.local_endpoint().empty(), true); + BOOST_TEST_EQ(sock.remote_endpoint().empty(), true); + } + + void testEndpointsBound() + { + io_context ioc(Backend); + local_datagram_socket sock(ioc); + sock.open(); + + test::temp_socket_dir tmp; + auto path = tmp.path(); + auto ec = sock.bind(local_endpoint(path)); + BOOST_TEST_EQ(!ec, true); + + BOOST_TEST_EQ(sock.local_endpoint().path(), path); + BOOST_TEST_EQ(sock.remote_endpoint().empty(), true); + } + + void testShutdown() + { + io_context ioc(Backend); + local_datagram_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + // Throwing overload (best-effort, may report ENOTCONN). + s1.shutdown(shutdown_send); + + // Non-throwing overload + std::error_code ec; + s2.shutdown(shutdown_send, ec); + + // Closed-socket no-ops + local_datagram_socket closed(ioc); + closed.shutdown(shutdown_send); + + std::error_code ec2; + closed.shutdown(shutdown_send, ec2); + BOOST_TEST_EQ(!ec2, true); + } + + void testBindClosedThrows() + { + io_context ioc(Backend); + local_datagram_socket sock(ioc); + + // NOLINTNEXTLINE(bugprone-unused-return-value) + BOOST_TEST_THROWS(sock.bind(local_endpoint("/tmp/never")), + std::logic_error); + } + + void testReleaseClosedThrows() + { + io_context ioc(Backend); + local_datagram_socket sock(ioc); + + bool caught = false; + try + { + (void)sock.release(); + } + catch (std::logic_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + + void testAvailableClosedThrows() + { + io_context ioc(Backend); + local_datagram_socket sock(ioc); + + bool caught = false; + try + { + (void)sock.available(); + } + catch (std::logic_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + + void testAvailable() + { + io_context ioc(Backend); + local_datagram_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + // Send a datagram, then check available on the receive side + auto ex = ioc.get_executor(); + char const msg[] = "hello"; + bool done = false; + capy::run_async(ex)( + [](local_datagram_socket& s, char const* m, std::size_t n, + bool& d) -> capy::task<> { + (void)co_await s.send(capy::const_buffer(m, n)); + d = true; + }(s1, msg, std::strlen(msg), done)); + + ioc.run(); + ioc.restart(); + + BOOST_TEST(done); + BOOST_TEST(s2.available() >= std::strlen(msg)); + } + + void testAssignAlreadyOpenThrows() + { + io_context ioc(Backend); + local_datagram_socket sock(ioc); + sock.open(); + + bool caught = false; + try + { + sock.assign(-1); + } + catch (std::logic_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + + void testRelease() + { + io_context ioc(Backend); + local_datagram_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + (void)s2; + BOOST_TEST(s1.is_open()); + + int fd = s1.release(); + BOOST_TEST(fd >= 0); + BOOST_TEST_EQ(s1.is_open(), false); + ::close(fd); + } + + void testSendOnClosedThrows() + { + io_context ioc(Backend); + local_datagram_socket sock(ioc); + char const m[] = "x"; + + bool caught_send = false; + try + { + (void)sock.send(capy::const_buffer(m, 1)); + } + catch (std::logic_error const&) + { + caught_send = true; + } + BOOST_TEST(caught_send); + + bool caught_send_to = false; + try + { + (void)sock.send_to( + capy::const_buffer(m, 1), local_endpoint("/tmp/x")); + } + catch (std::logic_error const&) + { + caught_send_to = true; + } + BOOST_TEST(caught_send_to); + + char buf[1]; + bool caught_recv = false; + try + { + (void)sock.recv(capy::mutable_buffer(buf, 1)); + } + catch (std::logic_error const&) + { + caught_recv = true; + } + BOOST_TEST(caught_recv); + + local_endpoint src; + bool caught_recv_from = false; + try + { + (void)sock.recv_from(capy::mutable_buffer(buf, 1), src); + } + catch (std::logic_error const&) + { + caught_recv_from = true; + } + BOOST_TEST(caught_recv_from); + } + + void testCancelPendingRecv() + { + io_context ioc(Backend); + local_datagram_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + (void)s1; + + auto ex = ioc.get_executor(); + std::error_code recv_ec; + bool recv_done = false; + + capy::run_async(ex)( + [](local_datagram_socket& s, + std::error_code& ec_out, bool& done) -> capy::task<> { + char buf[8]; + auto [ec, n] = co_await s.recv( + capy::mutable_buffer(buf, sizeof(buf))); + (void)n; + ec_out = ec; + done = true; + }(s2, recv_ec, recv_done)); + + auto canceller = [&]() -> capy::task<> { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(20)); + (void)co_await t.wait(); + s2.cancel(); + }; + capy::run_async(ex)(canceller()); + + ioc.run(); - cleanup_temp_socket(path1); - cleanup_temp_socket(path2); + BOOST_TEST(recv_done); + BOOST_TEST(recv_ec == capy::cond::canceled); } void run() @@ -511,6 +792,20 @@ struct local_datagram_socket_test testConstruction(); testOpen(); testMove(); + testMoveAssign(); + testCancelOnClosed(); + testNativeHandleClosed(); + testEndpointsClosed(); + testEndpointsBound(); + testShutdown(); + testBindClosedThrows(); + testReleaseClosedThrows(); + testAvailableClosedThrows(); + testAvailable(); + testAssignAlreadyOpenThrows(); + testRelease(); + testSendOnClosedThrows(); + testCancelPendingRecv(); testSendRecvConnected(); testExplicitBind(); testSendToRecvFrom(); diff --git a/test/unit/local_stream_socket.cpp b/test/unit/local_stream_socket.cpp index 0468e99aa..5962c5e2a 100644 --- a/test/unit/local_stream_socket.cpp +++ b/test/unit/local_stream_socket.cpp @@ -10,31 +10,38 @@ // Test that header file is self-contained. #include -#include +#include #include #include -#include +#include +#include #include -#include -#include -#include -#include #include #include #include +#include #include +#include +#include +#include -#include -#include -#include -#include +#include #if BOOST_COROSIO_POSIX #include +#else +#include #endif +#include +#include +#include +#include +#include +#include +#include + #include "context.hpp" -#include "local_temp.hpp" #include "test_suite.hpp" namespace boost::corosio { @@ -44,9 +51,6 @@ namespace boost::corosio { static_assert(capy::ReadStream); static_assert(capy::WriteStream); -using test::make_temp_socket_path; -using test::cleanup_temp_socket; - template struct local_stream_socket_test { @@ -85,7 +89,8 @@ struct local_stream_socket_test { io_context ioc(Backend); auto ex = ioc.get_executor(); - auto path = make_temp_socket_path(); + test::temp_socket_dir tmp; + auto path = tmp.path(); local_stream_acceptor acc(ioc); acc.open(); @@ -120,8 +125,6 @@ struct local_stream_socket_test ioc.run(); ioc.restart(); - cleanup_temp_socket(path); - BOOST_TEST_EQ(accept_done, true); BOOST_TEST_EQ(!accept_ec, true); BOOST_TEST_EQ(connect_done, true); @@ -134,7 +137,8 @@ struct local_stream_socket_test { io_context ioc(Backend); auto ex = ioc.get_executor(); - auto path = make_temp_socket_path(); + test::temp_socket_dir tmp; + auto path = tmp.path(); local_stream_acceptor acc(ioc); acc.open(); @@ -171,8 +175,6 @@ struct local_stream_socket_test ioc.run(); ioc.restart(); - cleanup_temp_socket(path); - BOOST_TEST_EQ(accept_done, true); BOOST_TEST_EQ(!accept_ec, true); BOOST_TEST_EQ(server_open, true); @@ -181,11 +183,12 @@ struct local_stream_socket_test } #if BOOST_COROSIO_POSIX - // Uses make_local_stream_pair, which is socketpair-based and POSIX-only. void testReadWrite() { io_context ioc(Backend); - auto [s1, s2] = make_local_stream_pair(ioc); + local_stream_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); auto ex = ioc.get_executor(); @@ -232,17 +235,21 @@ struct local_stream_socket_test void testSocketPair() { io_context ioc(Backend); - auto [s1, s2] = make_local_stream_pair(ioc); + local_stream_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); BOOST_TEST_EQ(s1.is_open(), true); BOOST_TEST_EQ(s2.is_open(), true); } #endif // BOOST_COROSIO_POSIX +#if BOOST_COROSIO_POSIX void testUnlinkExisting() { io_context ioc(Backend); - auto path = make_temp_socket_path(); + test::temp_socket_dir tmp; + auto path = tmp.path(); // First bind creates the socket file { @@ -268,8 +275,6 @@ struct local_stream_socket_test local_endpoint(path), bind_option::unlink_existing); BOOST_TEST_EQ(!ec, true); } - - cleanup_temp_socket(path); } void testUnlinkNonexistent() @@ -277,16 +282,16 @@ struct local_stream_socket_test // unlink_existing on a path that doesn't exist should // succeed (unlink silently fails with ENOENT). io_context ioc(Backend); - auto path = make_temp_socket_path(); + test::temp_socket_dir tmp; + auto path = tmp.path(); local_stream_acceptor acc(ioc); acc.open(); auto ec = acc.bind( local_endpoint(path), bind_option::unlink_existing); BOOST_TEST_EQ(!ec, true); - - cleanup_temp_socket(path); } +#endif void testEndpointOrdering() { @@ -318,19 +323,463 @@ struct local_stream_socket_test BOOST_TEST_EQ((a <=> b) == std::strong_ordering::less, true); } + void testMoveAssign() + { + io_context ioc(Backend); + local_stream_socket s1(ioc); + local_stream_socket s2(ioc); + s1.open(); + BOOST_TEST_EQ(s1.is_open(), true); + BOOST_TEST_EQ(s2.is_open(), false); + + s2 = std::move(s1); + BOOST_TEST_EQ(s2.is_open(), true); + BOOST_TEST_EQ(s1.is_open(), false); + + // Self-move-assign is a no-op + local_stream_socket& alias = s2; + s2 = std::move(alias); + BOOST_TEST_EQ(s2.is_open(), true); + } + + void testCancelOnClosedSocket() + { + io_context ioc(Backend); + local_stream_socket sock(ioc); + + // cancel() on a closed socket is a no-op (early return). + sock.cancel(); + BOOST_TEST_EQ(sock.is_open(), false); + } + + void testNativeHandleClosed() + { + io_context ioc(Backend); + local_stream_socket sock(ioc); + +#if BOOST_COROSIO_HAS_IOCP + auto const invalid = static_cast(~0ull); +#else + auto const invalid = static_cast(-1); +#endif + BOOST_TEST(sock.native_handle() == invalid); + + sock.open(); + BOOST_TEST(sock.native_handle() != invalid); + sock.close(); + } + + void testEndpointsClosed() + { + io_context ioc(Backend); + local_stream_socket sock(ioc); + + // Endpoints on a closed socket are empty defaults + BOOST_TEST_EQ(sock.local_endpoint().empty(), true); + BOOST_TEST_EQ(sock.remote_endpoint().empty(), true); + } + +#if BOOST_COROSIO_POSIX + void testEndpointsConnected() + { + io_context ioc(Backend); + test::temp_socket_dir tmp; + auto path = tmp.path(); + + local_stream_acceptor acc(ioc); + acc.open(); + auto ec = acc.bind(local_endpoint(path)); + BOOST_TEST_EQ(!ec, true); + ec = acc.listen(); + BOOST_TEST_EQ(!ec, true); + + local_stream_socket server(ioc); + local_stream_socket client(ioc); + auto ex = ioc.get_executor(); + + capy::run_async(ex)( + [](local_stream_acceptor& a, local_stream_socket& s) + -> capy::task<> { + (void)co_await a.accept(s); + }(acc, server)); + + capy::run_async(ex)( + [](local_stream_socket& s, local_endpoint ep) -> capy::task<> { + (void)co_await s.connect(ep); + }(client, local_endpoint(path))); + + ioc.run(); + ioc.restart(); + + // Endpoint accessors hit the backend + auto cl = client.local_endpoint(); + auto cr = client.remote_endpoint(); + auto sl = server.local_endpoint(); + auto sr = server.remote_endpoint(); + // server local should match the listening path + BOOST_TEST_EQ(sl.path(), path); + // client remote should match the listening path + BOOST_TEST_EQ(cr.path(), path); + // touch the others so the lines exec + (void)cl; + (void)sr; + } + + void testShutdown() + { + io_context ioc(Backend); + local_stream_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + // Throwing overload (best-effort) + s1.shutdown(shutdown_send); + + // Non-throwing overload + std::error_code ec; + s2.shutdown(shutdown_send, ec); + // ec may be unset or ENOTCONN depending on backend; we just want + // the code path exercised. The doc says best-effort. + + // Closed-socket variants + local_stream_socket closed(ioc); + closed.shutdown(shutdown_send); + + std::error_code ec2; + closed.shutdown(shutdown_send, ec2); + BOOST_TEST_EQ(!ec2, true); + } + + void testAssignAlreadyOpenThrows() + { + io_context ioc(Backend); + local_stream_socket sock(ioc); + sock.open(); + BOOST_TEST_EQ(sock.is_open(), true); + + bool caught = false; + try + { + sock.assign(-1); + } + catch (std::logic_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + + void testReleaseClosedThrows() + { + io_context ioc(Backend); + local_stream_socket sock(ioc); + + bool caught = false; + try + { + (void)sock.release(); + } + catch (std::logic_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + + void testAvailableClosedThrows() + { + io_context ioc(Backend); + local_stream_socket sock(ioc); + + bool caught = false; + try + { + (void)sock.available(); + } + catch (std::logic_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + + void testConnectToNonexistent() + { + // connect() to a path that doesn't exist should fail + // gracefully with an error in the awaitable, no throw. + io_context ioc(Backend); + auto ex = ioc.get_executor(); + // temp dir exists, but the socket file inside it does not + test::temp_socket_dir tmp; + auto path = tmp.path(); + + local_stream_socket client(ioc); + std::error_code result_ec; + bool done = false; + + capy::run_async(ex)( + [](local_stream_socket& s, local_endpoint ep, + std::error_code& ec_out, bool& d) -> capy::task<> { + auto [ec] = co_await s.connect(ep); + ec_out = ec; + d = true; + }(client, local_endpoint(path), result_ec, done)); + + ioc.run(); + + BOOST_TEST(done); + BOOST_TEST(!!result_ec); + } + + void testCancelPendingAccept() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + test::temp_socket_dir tmp; + auto path = tmp.path(); + + local_stream_acceptor acc(ioc); + acc.open(); + auto ec = acc.bind(local_endpoint(path)); + BOOST_TEST_EQ(!ec, true); + ec = acc.listen(); + BOOST_TEST_EQ(!ec, true); + + std::error_code accept_ec; + bool accept_done = false; + local_stream_socket server(ioc); + + capy::run_async(ex)( + [](local_stream_acceptor& a, local_stream_socket& s, + std::error_code& ec_out, bool& done) -> capy::task<> { + auto [ec] = co_await a.accept(s); + ec_out = ec; + done = true; + }(acc, server, accept_ec, accept_done)); + + // Schedule a cancel after a brief delay + auto canceller = [&]() -> capy::task<> { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(20)); + (void)co_await t.wait(); + acc.cancel(); + }; + capy::run_async(ex)(canceller()); + + ioc.run(); + + BOOST_TEST(accept_done); + BOOST_TEST(accept_ec == capy::cond::canceled); + } +#endif // BOOST_COROSIO_POSIX + + void testAcceptorOnClosedNoOp() + { + // cancel/close on a never-opened acceptor are no-ops. + io_context ioc(Backend); + local_stream_acceptor acc(ioc); + BOOST_TEST_EQ(acc.is_open(), false); + + acc.cancel(); + acc.close(); + BOOST_TEST_EQ(acc.is_open(), false); + + // local_endpoint() on a closed acceptor returns an empty endpoint. + BOOST_TEST_EQ(acc.local_endpoint().empty(), true); + } + + void testAcceptorBindClosedThrows() + { + io_context ioc(Backend); + local_stream_acceptor acc(ioc); + // NOLINTNEXTLINE(bugprone-unused-return-value) + BOOST_TEST_THROWS(acc.bind(local_endpoint("/tmp/never")), + std::logic_error); + } + + void testAcceptorListenClosedThrows() + { + io_context ioc(Backend); + local_stream_acceptor acc(ioc); + // NOLINTNEXTLINE(bugprone-unused-return-value) + BOOST_TEST_THROWS(acc.listen(), std::logic_error); + } + + void testAcceptorAcceptClosedThrows() + { + io_context ioc(Backend); + local_stream_acceptor acc(ioc); + local_stream_socket peer(ioc); + + bool caught_peer = false; + try + { + (void)acc.accept(peer); + } + catch (std::logic_error const&) + { + caught_peer = true; + } + BOOST_TEST(caught_peer); + + bool caught_move = false; + try + { + (void)acc.accept(); + } + catch (std::logic_error const&) + { + caught_move = true; + } + BOOST_TEST(caught_move); + } + + void testAcceptorReleaseClosedThrows() + { + io_context ioc(Backend); + local_stream_acceptor acc(ioc); + + bool caught = false; + try + { + (void)acc.release(); + } + catch (std::logic_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + + void testAcceptorReleaseOpen() + { + // release() returns the native fd and closes the acceptor + io_context ioc(Backend); + test::temp_socket_dir tmp; + auto path = tmp.path(); + + local_stream_acceptor acc(ioc); + acc.open(); + auto ec = acc.bind(local_endpoint(path)); + BOOST_TEST_EQ(!ec, true); + ec = acc.listen(); + BOOST_TEST_EQ(!ec, true); + + BOOST_TEST_EQ(acc.is_open(), true); + auto h = acc.release(); + (void)h; + BOOST_TEST_EQ(acc.is_open(), false); + +#if BOOST_COROSIO_POSIX + if (static_cast(h) >= 0) + ::close(static_cast(h)); +#endif + } + + void testAcceptorLocalEndpoint() + { + io_context ioc(Backend); + test::temp_socket_dir tmp; + auto path = tmp.path(); + + local_stream_acceptor acc(ioc); + acc.open(); + auto ec = acc.bind(local_endpoint(path)); + BOOST_TEST_EQ(!ec, true); + + auto ep = acc.local_endpoint(); + BOOST_TEST_EQ(ep.path(), path); + } + + void testEndpointTooLongThrows() + { + std::string too_long(local_endpoint::max_path_length + 1, 'x'); + bool caught = false; + try + { + local_endpoint ep(too_long); + (void)ep; + } + catch (std::system_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + + void testEndpointTooLongNoThrow() + { + std::string too_long(local_endpoint::max_path_length + 1, 'x'); + std::error_code ec; + local_endpoint ep(too_long, ec); + BOOST_TEST(!!ec); + BOOST_TEST_EQ(ep.empty(), true); + + // Successful construction with the no-throw overload clears ec. + std::error_code ec2; + local_endpoint ep2("/tmp/ok", ec2); + BOOST_TEST_EQ(!ec2, true); + BOOST_TEST_EQ(ep2.path(), std::string_view("/tmp/ok")); + } + + void testEndpointMaxPathLength() + { + // Exactly at the limit should succeed. + std::string at_limit(local_endpoint::max_path_length, 'a'); + local_endpoint ep(at_limit); + BOOST_TEST_EQ(ep.path().size(), local_endpoint::max_path_length); + } + +#ifdef __linux__ + void testAbstractEndpoint() + { + std::string abs_path(1, '\0'); + abs_path += "corosio_test_abstract_endpoint"; + local_endpoint ep(abs_path); + BOOST_TEST(ep.is_abstract()); + BOOST_TEST_EQ(ep.empty(), false); + } +#endif + void run() { testConstruction(); testOpen(); testMove(); + testMoveAssign(); + testCancelOnClosedSocket(); + testNativeHandleClosed(); + testEndpointsClosed(); testConnectAccept(); testMoveAccept(); #if BOOST_COROSIO_POSIX testReadWrite(); testSocketPair(); + testEndpointsConnected(); + testShutdown(); + testAssignAlreadyOpenThrows(); + testReleaseClosedThrows(); + testAvailableClosedThrows(); + testConnectToNonexistent(); + testCancelPendingAccept(); +#endif + testAcceptorOnClosedNoOp(); + testAcceptorBindClosedThrows(); + testAcceptorListenClosedThrows(); + testAcceptorAcceptClosedThrows(); + testAcceptorReleaseClosedThrows(); + testAcceptorReleaseOpen(); + testAcceptorLocalEndpoint(); + testEndpointTooLongThrows(); + testEndpointTooLongNoThrow(); + testEndpointMaxPathLength(); +#ifdef __linux__ + testAbstractEndpoint(); #endif +#if BOOST_COROSIO_POSIX testUnlinkExisting(); testUnlinkNonexistent(); +#endif testEndpointOrdering(); testEndpointStreamOutput(); #if BOOST_COROSIO_POSIX @@ -340,11 +789,12 @@ struct local_stream_socket_test } #if BOOST_COROSIO_POSIX - // Uses make_local_stream_pair, which is socketpair-based and POSIX-only. void testAvailable() { io_context ioc(Backend); - auto [s1, s2] = make_local_stream_pair(ioc); + local_stream_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); // Nothing written yet BOOST_TEST_EQ(s2.available(), std::size_t(0)); @@ -377,18 +827,27 @@ struct local_stream_socket_test void testRelease() { io_context ioc(Backend); - auto [s1, s2] = make_local_stream_pair(ioc); + local_stream_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); BOOST_TEST_EQ(s1.is_open(), true); - int fd = s1.release(); - BOOST_TEST_EQ(fd >= 0, true); + auto handle = s1.release(); BOOST_TEST_EQ(s1.is_open(), false); - // The released fd is still valid -- write through it + // The released handle is still valid -- write through it char const msg[] = "released"; - BOOST_TEST_EQ(::write(fd, msg, std::strlen(msg)) > 0, true); - ::close(fd); +#if BOOST_COROSIO_HAS_IOCP + BOOST_TEST_EQ( + ::send(static_cast(handle), + msg, static_cast(std::strlen(msg)), 0) > 0, true); + ::closesocket(static_cast(handle)); +#else + BOOST_TEST_EQ(handle >= 0, true); + BOOST_TEST_EQ(::write(handle, msg, std::strlen(msg)) > 0, true); + ::close(handle); +#endif } #endif diff --git a/test/unit/local_temp.hpp b/test/unit/local_temp.hpp deleted file mode 100644 index cc4b4d1c1..000000000 --- a/test/unit/local_temp.hpp +++ /dev/null @@ -1,68 +0,0 @@ -// -// Copyright (c) 2026 Steve Gerbino -// -// Distributed under the Boost Software License, Version 1.0. (See accompanying -// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) -// -// Official repository: https://github.com/cppalliance/corosio -// - -/* Portable temporary path helpers for Unix-domain socket tests. - - Replaces the POSIX-only mkdtemp/unlink/rmdir pattern with - std::filesystem so tests can run on Windows (which supports - AF_UNIX since Windows 10 build 17061). - - Paths are kept short — AF_UNIX limits sun_path to ~108 bytes - on POSIX and ~108 bytes on Windows. The "co_t_" prefix and - hex random suffix keep the total well under that limit on - typical Windows installations. -*/ - -#ifndef BOOST_COROSIO_TEST_LOCAL_TEMP_HPP -#define BOOST_COROSIO_TEST_LOCAL_TEMP_HPP - -#include -#include -#include -#include -#include -#include - -namespace boost::corosio::test { - -inline std::string -make_temp_socket_path(std::string_view prefix = "co_t_") -{ - namespace fs = std::filesystem; - - static std::random_device rd; - static std::mt19937_64 gen{rd()}; - - for (int attempt = 0; attempt < 16; ++attempt) - { - std::string name(prefix); - name += std::to_string(gen()); - - auto dir = fs::temp_directory_path() / name; - - std::error_code ec; - if (fs::create_directory(dir, ec)) - return (dir / "s").string(); - } - throw std::runtime_error("failed to create temp socket directory"); -} - -inline void -cleanup_temp_socket(std::string const& path) noexcept -{ - namespace fs = std::filesystem; - std::error_code ec; - fs::path p(path); - fs::remove(p, ec); - fs::remove(p.parent_path(), ec); -} - -} // namespace boost::corosio::test - -#endif diff --git a/test/unit/native/native_io_context.cpp b/test/unit/native/native_io_context.cpp index 9e4961a23..5862965fa 100644 --- a/test/unit/native/native_io_context.cpp +++ b/test/unit/native/native_io_context.cpp @@ -14,7 +14,6 @@ #include -#include "context.hpp" #include "test_suite.hpp" namespace boost::corosio { @@ -86,14 +85,84 @@ struct native_io_context_test BOOST_TEST(done); } + void testIoContextConstructWithOptions() + { + io_context_options opts; + opts.max_events_per_poll = 64; + opts.inline_budget_initial = 4; + opts.inline_budget_max = 16; + opts.unassisted_budget = 4; + + native_io_context ctx(opts, 2); + BOOST_TEST(!ctx.stopped()); + } + + void testIoContextRun() + { + native_io_context ctx; + auto ex = ctx.get_executor(); + + bool done = false; + auto task = [](bool& done_out) -> capy::task<> { + done_out = true; + co_return; + }; + capy::run_async(ex)(task(done)); + + auto n = ctx.run(); + BOOST_TEST(n > 0u); + BOOST_TEST(done); + } + + // Exercises the `rel_time > 1s` clamp branch in + // native_io_context::run_one_until. With no outstanding work, the + // scheduler stops immediately and returns 0 — the loop still entered + // with rel_time > 1s, hitting the clamp. + void testIoContextRunOneUntilLongDeadlineNoWork() + { + native_io_context ctx; + auto deadline = + std::chrono::steady_clock::now() + std::chrono::seconds(2); + auto n = ctx.run_one_until(deadline); + BOOST_TEST(n == 0u); + BOOST_TEST(ctx.stopped()); + } + + // Exercises the post-loop `return 0` after run_one_until times out. + // Outstanding work keeps the scheduler alive; the inner wait_one + // returns 0 each iteration until the deadline passes. + void testIoContextRunForWithOutstandingWork() + { + native_io_context ctx; + auto ex = ctx.get_executor(); + + ex.on_work_started(); + + auto start = std::chrono::steady_clock::now(); + std::size_t n = ctx.run_for(std::chrono::milliseconds(50)); + auto elapsed = std::chrono::steady_clock::now() - start; + + BOOST_TEST(n == 0u); + auto ms = std::chrono::duration_cast(elapsed) + .count(); + BOOST_TEST(ms >= 30); + BOOST_TEST(ms < 1000); + + ex.on_work_finished(); + } + void run() { testIoContextConstruct(); testIoContextConstructHint(); + testIoContextConstructWithOptions(); testIoContextPolymorphicSlice(); testIoContextPoll(); testIoContextStopRestart(); + testIoContextRun(); testIoContextRunFor(); + testIoContextRunOneUntilLongDeadlineNoWork(); + testIoContextRunForWithOutstandingWork(); } }; diff --git a/test/unit/native/native_local_datagram_socket.cpp b/test/unit/native/native_local_datagram_socket.cpp index 7dab48906..c28c646ca 100644 --- a/test/unit/native/native_local_datagram_socket.cpp +++ b/test/unit/native/native_local_datagram_socket.cpp @@ -18,25 +18,22 @@ #include #include +#include #include #include #include #include -#include +#include #include #include #include "context.hpp" -#include "local_temp.hpp" #include "test_suite.hpp" namespace boost::corosio { -using test::make_temp_socket_path; -using test::cleanup_temp_socket; - template struct native_local_datagram_socket_test { @@ -118,8 +115,10 @@ struct native_local_datagram_socket_test void testSendToRecvFrom() { io_context ioc(Backend); - auto path1 = make_temp_socket_path(); - auto path2 = make_temp_socket_path(); + test::temp_socket_dir tmp1; + test::temp_socket_dir tmp2; + auto path1 = tmp1.path(); + auto path2 = tmp2.path(); native_local_datagram_socket sender(ioc); native_local_datagram_socket receiver(ioc); @@ -153,16 +152,15 @@ struct native_local_datagram_socket_test auto ex = ioc.get_executor(); capy::run_async(ex)(task(sender, receiver, local_endpoint(path2))); ioc.run(); - - cleanup_temp_socket(path1); - cleanup_temp_socket(path2); } void testSendRecvConnected() { io_context ioc(Backend); - auto path_a = make_temp_socket_path(); - auto path_b = make_temp_socket_path(); + test::temp_socket_dir tmp_a; + test::temp_socket_dir tmp_b; + auto path_a = tmp_a.path(); + auto path_b = tmp_b.path(); native_local_datagram_socket a(ioc); native_local_datagram_socket b(ioc); @@ -202,16 +200,15 @@ struct native_local_datagram_socket_test capy::run_async(ex)(task( a, b, local_endpoint(path_b), local_endpoint(path_a))); ioc.run(); - - cleanup_temp_socket(path_a); - cleanup_temp_socket(path_b); } void testVirtualDispatchFallback() { io_context ioc(Backend); - auto path1 = make_temp_socket_path(); - auto path2 = make_temp_socket_path(); + test::temp_socket_dir tmp1; + test::temp_socket_dir tmp2; + auto path1 = tmp1.path(); + auto path2 = tmp2.path(); native_local_datagram_socket sender(ioc); native_local_datagram_socket receiver(ioc); @@ -244,9 +241,6 @@ struct native_local_datagram_socket_test auto ex = ioc.get_executor(); capy::run_async(ex)(task(s_ref, r_ref, local_endpoint(path2))); ioc.run(); - - cleanup_temp_socket(path1); - cleanup_temp_socket(path2); } // Exercise the shadowed wait() awaitable: wait_type::read on a @@ -255,7 +249,8 @@ struct native_local_datagram_socket_test { io_context ioc(Backend); auto ex = ioc.get_executor(); - auto rx_path = test::make_temp_socket_path(); + test::temp_socket_dir rx_tmp; + auto rx_path = rx_tmp.path(); native_local_datagram_socket recv(ioc); recv.open(); @@ -286,12 +281,97 @@ struct native_local_datagram_socket_test capy::run_async(ex)(sender()); ioc.run(); - test::cleanup_temp_socket(rx_path); - BOOST_TEST(wait_done); BOOST_TEST(!wait_ec); } + void testSendOnClosedThrows() + { + io_context ioc(Backend); + native_local_datagram_socket s(ioc); + char const m[] = "x"; + + bool caught_send = false; + try + { + (void)s.send(capy::const_buffer(m, 1)); + } + catch (std::logic_error const&) + { + caught_send = true; + } + BOOST_TEST(caught_send); + + bool caught_send_to = false; + try + { + (void)s.send_to( + capy::const_buffer(m, 1), local_endpoint("/tmp/x")); + } + catch (std::logic_error const&) + { + caught_send_to = true; + } + BOOST_TEST(caught_send_to); + + char buf[1]; + bool caught_recv = false; + try + { + (void)s.recv(capy::mutable_buffer(buf, 1)); + } + catch (std::logic_error const&) + { + caught_recv = true; + } + BOOST_TEST(caught_recv); + + local_endpoint src; + bool caught_recv_from = false; + try + { + (void)s.recv_from(capy::mutable_buffer(buf, 1), src); + } + catch (std::logic_error const&) + { + caught_recv_from = true; + } + BOOST_TEST(caught_recv_from); + } + + void testConnectAutoOpens() + { + // connect() on a closed socket should auto-open then attempt to + // connect. We aim it at a path that does not exist so the + // connect itself yields an error, but the auto-open branch + // is exercised. + io_context ioc(Backend); + auto ex = ioc.get_executor(); + // temp dir exists, but the socket file inside it does not + test::temp_socket_dir tmp; + auto path = tmp.path(); + + native_local_datagram_socket s(ioc); + BOOST_TEST_EQ(s.is_open(), false); + + std::error_code result_ec; + bool done = false; + + capy::run_async(ex)( + [](native_local_datagram_socket& sock, + local_endpoint ep, + std::error_code& ec_out, bool& d) -> capy::task<> { + auto [ec] = co_await sock.connect(ep); + ec_out = ec; + d = true; + }(s, local_endpoint(path), result_ec, done)); + + ioc.run(); + BOOST_TEST(done); + // socket was opened by the connect() call + BOOST_TEST(s.is_open()); + } + void run() { testConstruct(); @@ -301,6 +381,8 @@ struct native_local_datagram_socket_test testSendRecvConnected(); testVirtualDispatchFallback(); testWait(); + testSendOnClosedThrows(); + testConnectAutoOpens(); } }; diff --git a/test/unit/native/native_local_stream_socket.cpp b/test/unit/native/native_local_stream_socket.cpp index 0db9a2166..334b1cb0b 100644 --- a/test/unit/native/native_local_stream_socket.cpp +++ b/test/unit/native/native_local_stream_socket.cpp @@ -12,24 +12,22 @@ #include #include #include +#include #include #include #include +#include #include #include #include #include "context.hpp" -#include "local_temp.hpp" #include "test_suite.hpp" namespace boost::corosio { -using test::make_temp_socket_path; -using test::cleanup_temp_socket; - template struct native_local_stream_socket_test { @@ -111,7 +109,8 @@ struct native_local_stream_socket_test void testConnectAcceptReadWrite() { io_context ioc(Backend); - auto path = make_temp_socket_path(); + test::temp_socket_dir tmp; + auto path = tmp.path(); native_local_stream_acceptor acc(ioc); acc.open(); @@ -152,14 +151,13 @@ struct native_local_stream_socket_test capy::run_async(ex)(acceptor_task(acc, server)); capy::run_async(ex)(client_task(client, local_endpoint(path))); ioc.run(); - - cleanup_temp_socket(path); } void testMoveAccept() { io_context ioc(Backend); - auto path = make_temp_socket_path(); + test::temp_socket_dir tmp; + auto path = tmp.path(); native_local_stream_acceptor acc(ioc); acc.open(); @@ -199,14 +197,13 @@ struct native_local_stream_socket_test capy::run_async(ex)(acceptor_task(acc)); capy::run_async(ex)(client_task(client, local_endpoint(path))); ioc.run(); - - cleanup_temp_socket(path); } void testVirtualDispatchFallback() { io_context ioc(Backend); - auto path = make_temp_socket_path(); + test::temp_socket_dir tmp; + auto path = tmp.path(); native_local_stream_acceptor acc(ioc); acc.open(); @@ -248,8 +245,6 @@ struct native_local_stream_socket_test capy::run_async(ex)(acceptor_task(acc_ref, server_ref)); capy::run_async(ex)(client_task(client_ref, local_endpoint(path))); ioc.run(); - - cleanup_temp_socket(path); } // Exercise the shadowed wait() awaitable on the socket: a @@ -259,7 +254,8 @@ struct native_local_stream_socket_test { io_context ioc(Backend); auto ex = ioc.get_executor(); - auto path = test::make_temp_socket_path(); + test::temp_socket_dir tmp; + auto path = tmp.path(); native_local_stream_acceptor acc(ioc); acc.open(); @@ -295,8 +291,6 @@ struct native_local_stream_socket_test capy::run_async(ex)(waiter()); ioc.run(); - test::cleanup_temp_socket(path); - BOOST_TEST(wait_done); BOOST_TEST(!wait_ec); } @@ -307,7 +301,8 @@ struct native_local_stream_socket_test { io_context ioc(Backend); auto ex = ioc.get_executor(); - auto path = test::make_temp_socket_path(); + test::temp_socket_dir tmp; + auto path = tmp.path(); native_local_stream_acceptor acc(ioc); acc.open(); @@ -334,12 +329,67 @@ struct native_local_stream_socket_test capy::run_async(ex)(connect_task()); ioc.run(); - test::cleanup_temp_socket(path); - BOOST_TEST(wait_done); BOOST_TEST(!wait_ec); } + void testAcceptOnClosedThrows() + { + io_context ioc(Backend); + native_local_stream_acceptor acc(ioc); + native_local_stream_socket peer(ioc); + + bool caught_peer = false; + try + { + (void)acc.accept(peer); + } + catch (std::logic_error const&) + { + caught_peer = true; + } + BOOST_TEST(caught_peer); + + bool caught_move = false; + try + { + (void)acc.accept(); + } + catch (std::logic_error const&) + { + caught_move = true; + } + BOOST_TEST(caught_move); + } + + void testConnectAutoOpens() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + // temp dir exists, but the socket file inside it does not + test::temp_socket_dir tmp; + auto path = tmp.path(); + + native_local_stream_socket s(ioc); + BOOST_TEST_EQ(s.is_open(), false); + + std::error_code result_ec; + bool done = false; + + capy::run_async(ex)( + [](native_local_stream_socket& sock, + local_endpoint ep, + std::error_code& ec_out, bool& d) -> capy::task<> { + auto [ec] = co_await sock.connect(ep); + ec_out = ec; + d = true; + }(s, local_endpoint(path), result_ec, done)); + + ioc.run(); + BOOST_TEST(done); + BOOST_TEST(s.is_open()); + } + void run() { testConstruct(); @@ -350,6 +400,8 @@ struct native_local_stream_socket_test testVirtualDispatchFallback(); testSocketWait(); testAcceptorWait(); + testAcceptOnClosedThrows(); + testConnectAutoOpens(); } }; diff --git a/test/unit/native/native_resolver.cpp b/test/unit/native/native_resolver.cpp index 4969357c8..edcc9f6d9 100644 --- a/test/unit/native/native_resolver.cpp +++ b/test/unit/native/native_resolver.cpp @@ -10,14 +10,19 @@ #include #include +#include #include #include #include +#include #include #include -#include "context.hpp" +#if BOOST_COROSIO_POSIX +#include +#endif + #include "test_suite.hpp" namespace boost::corosio { @@ -90,6 +95,48 @@ struct native_resolver_test } }; +#if BOOST_COROSIO_POSIX +// Direct coverage of make_gai_error switch arms. Real getaddrinfo() +// implementations rarely return anything but EAI_NONAME for synthetic +// inputs, so we drive the helper directly. +struct posix_resolver_gai_error_test +{ + static void check_mapped(int gai_err, std::errc expected) + { + auto ec = detail::posix_resolver_detail::make_gai_error(gai_err); + BOOST_TEST(ec == expected); + } + + void testEachArm() + { + check_mapped(EAI_AGAIN, std::errc::resource_unavailable_try_again); + check_mapped(EAI_BADFLAGS, std::errc::invalid_argument); + check_mapped(EAI_FAIL, std::errc::io_error); + check_mapped(EAI_FAMILY, std::errc::address_family_not_supported); + check_mapped(EAI_MEMORY, std::errc::not_enough_memory); + check_mapped(EAI_SERVICE, std::errc::invalid_argument); + check_mapped(EAI_SOCKTYPE, std::errc::not_supported); + + // EAI_SYSTEM reads errno; force a known value first. + errno = EINVAL; + auto sys_ec = detail::posix_resolver_detail::make_gai_error(EAI_SYSTEM); + BOOST_TEST(sys_ec == std::errc::invalid_argument); + + // Default arm: unknown gai value falls through to io_error. + check_mapped(9999, std::errc::io_error); + } + + void run() + { + testEachArm(); + } +}; + +TEST_SUITE( + posix_resolver_gai_error_test, + "boost.corosio.native.resolver.posix.make_gai_error"); +#endif + #if BOOST_COROSIO_HAS_EPOLL struct native_resolver_test_epoll : native_resolver_test {}; diff --git a/test/unit/native/native_tcp_acceptor.cpp b/test/unit/native/native_tcp_acceptor.cpp index 5c361a880..4c4fab003 100644 --- a/test/unit/native/native_tcp_acceptor.cpp +++ b/test/unit/native/native_tcp_acceptor.cpp @@ -19,7 +19,6 @@ #include #include -#include "context.hpp" #include "test_suite.hpp" namespace boost::corosio { @@ -120,12 +119,32 @@ struct native_tcp_acceptor_test BOOST_TEST(!wait_ec); } +#ifdef SO_REUSEPORT + void testNativeReusePort() + { + io_context ctx(Backend); + native_tcp_acceptor acc(ctx); + acc.open(); + + acc.set_option(native_socket_option::reuse_address(true)); + acc.set_option(native_socket_option::reuse_port(true)); + auto rp = + acc.template get_option(); + BOOST_TEST(rp.value()); + + acc.close(); + } +#endif + void run() { testAcceptorConstruct(); testAcceptorMoveConstruct(); testAcceptorPolymorphicSlice(); testWait(); +#ifdef SO_REUSEPORT + testNativeReusePort(); +#endif } }; diff --git a/test/unit/native/native_tcp_socket.cpp b/test/unit/native/native_tcp_socket.cpp index fa188433c..496dc657e 100644 --- a/test/unit/native/native_tcp_socket.cpp +++ b/test/unit/native/native_tcp_socket.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -20,7 +21,6 @@ #include #include -#include "context.hpp" #include "test_suite.hpp" namespace boost::corosio { @@ -124,12 +124,43 @@ struct native_tcp_socket_test BOOST_TEST(!wait_ec); } + // Exercise inline native_socket_option types on a TCP socket so the + // boolean instantiation is fully hit. + void testNativeNoDelay() + { + io_context ctx(Backend); + native_tcp_socket s(ctx); + s.open(); + + s.set_option(native_socket_option::no_delay(true)); + auto nd = s.template get_option(); + BOOST_TEST(nd.value()); + + s.set_option(native_socket_option::no_delay(false)); + nd = s.template get_option(); + BOOST_TEST(!nd.value()); + + // Cover member accessors on the inline boolean<>. + native_socket_option::no_delay direct(true); + BOOST_TEST(direct.value()); + BOOST_TEST_EQ(direct.size(), sizeof(int)); + BOOST_TEST(direct.data() != nullptr); + BOOST_TEST_EQ(direct.level(), IPPROTO_TCP); + BOOST_TEST_EQ(direct.name(), TCP_NODELAY); + + native_socket_option::no_delay const& cd = direct; + BOOST_TEST(cd.data() != nullptr); + + s.close(); + } + void run() { testSocketConstruct(); testSocketMoveConstruct(); testSocketPolymorphicSlice(); testWait(); + testNativeNoDelay(); } }; diff --git a/test/unit/native/native_udp_socket.cpp b/test/unit/native/native_udp_socket.cpp index 410d6df6d..13cc842a8 100644 --- a/test/unit/native/native_udp_socket.cpp +++ b/test/unit/native/native_udp_socket.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -21,7 +22,6 @@ #include #include -#include "context.hpp" #include "test_suite.hpp" namespace boost::corosio { @@ -385,6 +385,171 @@ struct native_udp_socket_test BOOST_TEST(!wait_ec); } + // Exercise the inline native_socket_option types on a UDP socket. + // The public socket_option:: variants are tested elsewhere; this + // hits the templated boolean<>/integer<> instantiations and the + // multicast option storage classes without library indirection. + void testNativeSocketOptions() + { + io_context ioc(Backend); + native_udp_socket sock(ioc); + sock.open(); + + sock.set_option(native_socket_option::broadcast(true)); + auto bc = + sock.template get_option(); + BOOST_TEST(bc.value()); + + sock.set_option(native_socket_option::receive_buffer_size(32768)); + auto rb = sock.template get_option< + native_socket_option::receive_buffer_size>(); + BOOST_TEST(rb.value() > 0); + + sock.set_option(native_socket_option::send_buffer_size(32768)); + auto sb = sock.template get_option< + native_socket_option::send_buffer_size>(); + BOOST_TEST(sb.value() > 0); + + sock.set_option(native_socket_option::multicast_loop_v4(true)); + auto ml = sock.template get_option< + native_socket_option::multicast_loop_v4>(); + BOOST_TEST(ml.value()); + + sock.set_option(native_socket_option::multicast_hops_v4(4)); + auto mh = sock.template get_option< + native_socket_option::multicast_hops_v4>(); + BOOST_TEST_EQ(mh.value(), 4); + + // Multicast configuration is environment-specific; the option + // type's storage and the library set_option path are exercised + // regardless of whether the kernel completes the request. + try + { + sock.set_option(native_socket_option::multicast_interface_v4( + ipv4_address::any())); + } + catch (std::system_error const&) + { + BOOST_TEST_PASS(); + } + + // Default-constructed variants exercise the no-arg constructors. + native_socket_option::multicast_interface_v4 mif4; + BOOST_TEST_EQ(mif4.size(), sizeof(struct in_addr)); + BOOST_TEST(mif4.data() != nullptr); + BOOST_TEST_EQ(mif4.level(), IPPROTO_IP); + BOOST_TEST_EQ(mif4.name(), IP_MULTICAST_IF); + + sock.close(); + } + + void testNativeMulticastV4Groups() + { + io_context ioc(Backend); + native_udp_socket sock(ioc); + sock.open(); + + try + { + sock.set_option(native_socket_option::join_group_v4( + ipv4_address("239.255.0.3"))); + sock.set_option(native_socket_option::leave_group_v4( + ipv4_address("239.255.0.3"))); + } + catch (std::system_error const&) + { + BOOST_TEST_PASS(); + } + + // Default-constructed forms — verifies no-arg ctor coverage. + native_socket_option::join_group_v4 jg; + BOOST_TEST_EQ(jg.size(), sizeof(struct ip_mreq)); + BOOST_TEST(jg.data() != nullptr); + BOOST_TEST_EQ(jg.level(), IPPROTO_IP); + BOOST_TEST_EQ(jg.name(), IP_ADD_MEMBERSHIP); + + native_socket_option::leave_group_v4 lg; + BOOST_TEST_EQ(lg.size(), sizeof(struct ip_mreq)); + BOOST_TEST(lg.data() != nullptr); + BOOST_TEST_EQ(lg.level(), IPPROTO_IP); + BOOST_TEST_EQ(lg.name(), IP_DROP_MEMBERSHIP); + + sock.close(); + } + + void testNativeMulticastV6Groups() + { + io_context ioc(Backend); + native_udp_socket sock(ioc); + sock.open(udp::v6()); + + sock.set_option(native_socket_option::multicast_loop_v6(true)); + sock.set_option(native_socket_option::multicast_hops_v6(4)); + + try + { + sock.set_option(native_socket_option::multicast_interface_v6(0)); + auto mif6 = sock.template get_option< + native_socket_option::multicast_interface_v6>(); + BOOST_TEST_EQ(mif6.value(), 0); + } + catch (std::system_error const&) + { + BOOST_TEST_PASS(); + } + + try + { + sock.set_option(native_socket_option::join_group_v6( + ipv6_address("ff02::1"))); + sock.set_option(native_socket_option::leave_group_v6( + ipv6_address("ff02::1"))); + } + catch (std::system_error const&) + { + BOOST_TEST_PASS(); + } + + // Default-constructed forms — verifies no-arg ctor coverage. + native_socket_option::join_group_v6 jg; + BOOST_TEST_EQ(jg.size(), sizeof(struct ipv6_mreq)); + BOOST_TEST(jg.data() != nullptr); + BOOST_TEST_EQ(jg.level(), IPPROTO_IPV6); + BOOST_TEST_EQ(jg.name(), IPV6_JOIN_GROUP); + + native_socket_option::leave_group_v6 lg; + BOOST_TEST_EQ(lg.size(), sizeof(struct ipv6_mreq)); + BOOST_TEST(lg.data() != nullptr); + BOOST_TEST_EQ(lg.level(), IPPROTO_IPV6); + BOOST_TEST_EQ(lg.name(), IPV6_LEAVE_GROUP); + + sock.close(); + } + + void testNativeLinger() + { + // Exercises native_socket_option::linger (default + member setters). + native_socket_option::linger lg; + BOOST_TEST(!lg.enabled()); + BOOST_TEST_EQ(lg.timeout(), 0); + BOOST_TEST_EQ(lg.size(), sizeof(::linger)); + BOOST_TEST(lg.data() != nullptr); + + native_socket_option::linger const& clg = lg; + BOOST_TEST(clg.data() != nullptr); + + lg.enabled(true); + lg.timeout(7); + BOOST_TEST(lg.enabled()); + BOOST_TEST_EQ(lg.timeout(), 7); + BOOST_TEST_EQ(lg.level(), SOL_SOCKET); + BOOST_TEST_EQ(lg.name(), SO_LINGER); + + native_socket_option::linger lg2(true, 3); + BOOST_TEST(lg2.enabled()); + BOOST_TEST_EQ(lg2.timeout(), 3); + } + void run() { testConstruct(); @@ -397,6 +562,10 @@ struct native_udp_socket_test testCloseWhileRecving(); testVirtualDispatchFallback(); testWait(); + testNativeSocketOptions(); + testNativeMulticastV4Groups(); + testNativeMulticastV6Groups(); + testNativeLinger(); } }; diff --git a/test/unit/openssl_stream.cpp b/test/unit/openssl_stream.cpp index 341759763..0260ff5db 100644 --- a/test/unit/openssl_stream.cpp +++ b/test/unit/openssl_stream.cpp @@ -54,6 +54,28 @@ struct openssl_stream_test BOOST_TEST(stream.name() == "openssl"); } + /** Exercise next_layer() accessors (const and non-const). */ + void testNextLayer() + { + using namespace test; + + io_context ioc; + auto ctx = make_anon_context(); + tcp_socket sock(ioc); + openssl_stream stream(&sock, ctx); + + // Non-const overload via mutable stream. + capy::any_stream& mutable_next = stream.next_layer(); + (void)mutable_next; + + // Const overload via reference to const. + openssl_stream const& cref = stream; + capy::any_stream const& const_next = cref.next_layer(); + (void)const_next; + + BOOST_TEST(&mutable_next == &const_next); + } + /** Test certificate chain validation (OpenSSL-specific). OpenSSL supports sending full certificate chains via @@ -103,6 +125,7 @@ struct openssl_stream_test testCertificateChain(); testName(); + testNextLayer(); } }; diff --git a/test/unit/random_access_file.cpp b/test/unit/random_access_file.cpp index a677cee7d..46b494a77 100644 --- a/test/unit/random_access_file.cpp +++ b/test/unit/random_access_file.cpp @@ -355,7 +355,7 @@ struct random_access_file_test int& count_out) -> capy::task<> { char buf[4]; - for (int i = 0; i < 4; ++i) + for (std::uint64_t i = 0; i < 4; ++i) { auto [ec, n] = co_await f_ref.read_some_at( i * 4, capy::mutable_buffer(buf, 4)); @@ -385,6 +385,48 @@ struct random_access_file_test BOOST_TEST_PASS(); } + void testCancelOnClosedFile() + { + io_context ioc; + random_access_file f(ioc); + + // cancel() on a closed file is a no-op (early return). + f.cancel(); + BOOST_TEST(!f.is_open()); + } + + void testNativeHandleClosedAndOpen() + { + temp_file tmp("raf_nh_", "x"); + io_context ioc; + random_access_file f(ioc); + +#if BOOST_COROSIO_HAS_IOCP + auto const invalid = static_cast(~0ull); +#else + auto const invalid = static_cast(-1); +#endif + BOOST_TEST(f.native_handle() == invalid); + + f.open(tmp.path, file_base::read_only); + BOOST_TEST(f.native_handle() != invalid); + } + + void testOpenReplacesExisting() + { + temp_file tmp1("raf_replace_a_", "first"); + temp_file tmp2("raf_replace_b_", "second"); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp1.path, file_base::read_only); + BOOST_TEST(f.is_open()); + + // Reopen on an already-open file closes the previous handle. + f.open(tmp2.path, file_base::read_only); + BOOST_TEST(f.is_open()); + } + // Sync data void testSyncData() @@ -536,10 +578,10 @@ struct random_access_file_test void testManyConcurrentOps() { // Stress test: 100 concurrent reads - constexpr int num_ops = 100; - constexpr int block_sz = 4; + constexpr std::size_t num_ops = 100; + constexpr std::size_t block_sz = 4; std::string data(num_ops * block_sz, 'X'); - for (int i = 0; i < num_ops; ++i) + for (std::size_t i = 0; i < num_ops; ++i) std::memset(data.data() + i * block_sz, 'A' + (i % 26), block_sz); @@ -558,12 +600,12 @@ struct random_access_file_test off, capy::mutable_buffer(buf, block_sz)); BOOST_TEST(!ec); BOOST_TEST_EQ(n, static_cast(block_sz)); - for (int i = 0; i < block_sz; ++i) + for (std::size_t i = 0; i < block_sz; ++i) BOOST_TEST_EQ(buf[i], expected); ++count; }; - for (int i = 0; i < num_ops; ++i) + for (std::size_t i = 0; i < num_ops; ++i) { capy::run_async(ioc.get_executor())( reader(f, i * block_sz, @@ -597,6 +639,9 @@ struct random_access_file_test testSequentialReads(); testCancelNoOperation(); + testCancelOnClosedFile(); + testNativeHandleClosedAndOpen(); + testOpenReplacesExisting(); testSyncData(); testConcurrentReads(); @@ -608,6 +653,12 @@ struct random_access_file_test testRelease(); testAssign(); testClosedFileThrows(); + testOpenSyncAllOnWrite(); + testOpenExclusiveExistingFails(); + testOpenExclusiveNewFile(); + testEmptyBufferReadWrite(); + testReadAtPastEofErrorPath(); + testCancelInflightOperation(); testCancelWithStoppedToken(); } @@ -633,6 +684,160 @@ struct random_access_file_test expect_throw([&] { f.release(); }); } + // Open flag variants + + void testOpenSyncAllOnWrite() + { + // Exercises the O_SYNC mapping in posix_random_access_file::open_file. + temp_file tmp("raf_sync_open_"); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, + file_base::write_only | file_base::create + | file_base::truncate | file_base::sync_all_on_write); + BOOST_TEST(f.is_open()); + + bool done = false; + auto task = [](random_access_file& f_ref, + bool& d) -> capy::task<> { + auto [ec, n] = co_await f_ref.write_some_at( + 0, capy::const_buffer("synced", 6)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 6u); + d = true; + }; + capy::run_async(ioc.get_executor())(task(f, done)); + ioc.run(); + BOOST_TEST(done); + } + + void testOpenExclusiveExistingFails() + { + // create|exclusive on an existing file maps to O_EXCL and + // surfaces EEXIST. + temp_file tmp("raf_excl_", "x"); + io_context ioc; + random_access_file f(ioc); + + bool threw = false; + try + { + f.open(tmp.path, + file_base::write_only | file_base::create + | file_base::exclusive); + } + catch (std::system_error const&) + { + threw = true; + } + BOOST_TEST(threw); + BOOST_TEST(!f.is_open()); + } + + void testOpenExclusiveNewFile() + { + // create|exclusive on a new file path succeeds and exercises + // the O_EXCL flag mapping. + temp_file tmp("raf_excl_new_"); // no contents, file does not exist + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, + file_base::write_only | file_base::create + | file_base::exclusive); + BOOST_TEST(f.is_open()); + f.close(); + } + + void testEmptyBufferReadWrite() + { + // Zero-byte read/write short-circuits before the pool dispatch + // (early return at the top of read_some_at/write_some_at). + temp_file tmp("raf_empty_", "hi"); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, file_base::read_write); + + bool done = false; + auto task = [](random_access_file& f_ref, bool& d) -> capy::task<> { + auto [rec, rn] = co_await f_ref.read_some_at( + 0, capy::mutable_buffer(nullptr, 0)); + BOOST_TEST(!rec); + BOOST_TEST_EQ(rn, 0u); + + auto [wec, wn] = co_await f_ref.write_some_at( + 0, capy::const_buffer(nullptr, 0)); + BOOST_TEST(!wec); + BOOST_TEST_EQ(wn, 0u); + d = true; + }; + capy::run_async(ioc.get_executor())(task(f, done)); + ioc.run(); + BOOST_TEST(done); + } + + void testCancelInflightOperation() + { + // Launch many concurrent reads, then call cancel() to mark the + // outstanding_ops_ list. Exercises the for_each callback that + // stamps each op's cancelled flag. + std::string data(std::size_t{64} * 1024, 'X'); + temp_file tmp("raf_cancel_inflight_", data); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, file_base::read_only); + + constexpr std::uint64_t num_ops = 16; + std::atomic completed{0}; + + auto reader = [](random_access_file* f, std::uint64_t off, + std::atomic* c) -> capy::task<> { + char buf[1024]; + auto [ec, n] = + co_await f->read_some_at(off, capy::mutable_buffer(buf, 1024)); + (void)ec; + (void)n; + c->fetch_add(1); + }; + + for (std::uint64_t i = 0; i < num_ops; ++i) + capy::run_async(ioc.get_executor())(reader(&f, i * 1024, &completed)); + + // Immediately cancel before any op can complete. + f.cancel(); + + ioc.run(); + + BOOST_TEST_EQ(completed.load(), num_ops); + } + + void testReadAtPastEofErrorPath() + { + // Reading past end returns eof error code via the op-completion + // bytes_transferred==0 branch (different from explicit EOF earlier). + temp_file tmp("raf_pasteof_", "abc"); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, file_base::read_only); + + bool done = false; + auto task = [](random_access_file& f_ref, bool& d) -> capy::task<> { + char buf[16]; + auto [ec, n] = co_await f_ref.read_some_at( + 1000, capy::mutable_buffer(buf, sizeof(buf))); + BOOST_TEST(ec); // EOF or io error - non-zero size, zero read + BOOST_TEST_EQ(n, 0u); + d = true; + }; + capy::run_async(ioc.get_executor())(task(f, done)); + ioc.run(); + BOOST_TEST(done); + } + // Cancellation void testCancelWithStoppedToken() diff --git a/test/unit/reactor_paths.cpp b/test/unit/reactor_paths.cpp new file mode 100644 index 000000000..b1b33f0fc --- /dev/null +++ b/test/unit/reactor_paths.cpp @@ -0,0 +1,1514 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +// Coverage tests for reactor-backend code paths that are not exercised by +// the per-type unit tests. Targets: multi-op queueing on the same fd, +// wait_type::error completions, scatter-gather (multi-buffer) I/O, acceptor +// wait variants, datagram zero-length receive, shutdown variants, and +// close-during-pending-op races. + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#if BOOST_COROSIO_POSIX +#include +#include +#include +#include + +#include +#include +#endif + +#include + +#include "context.hpp" +#include "test_suite.hpp" + +namespace boost::corosio { + +template +struct reactor_paths_test +{ + // Spawn a read and a write coroutine on the same socket concurrently. + // Exercises the descriptor_state branch where a single edge event + // contains both read and write readiness for the same fd. + void testConcurrentReadWrite() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + auto [s1, s2] = + test::make_socket_pair(ioc); + + constexpr std::string_view payload = "concurrent"; + char read_buf[64] = {}; + + std::error_code read_ec; + std::error_code write_ec; + std::size_t read_n = 0; + std::size_t write_n = 0; + + auto reader = [&]() -> capy::task<> { + auto [ec, n] = co_await s1.read_some( + capy::mutable_buffer(read_buf, sizeof(read_buf))); + read_ec = ec; + read_n = n; + }; + auto writer = [&]() -> capy::task<> { + auto [ec, n] = co_await s1.write_some( + capy::const_buffer(payload.data(), payload.size())); + write_ec = ec; + write_n = n; + }; + auto peer_writer = [&]() -> capy::task<> { + // Brief delay so the read side parks first. + timer t(ioc); + t.expires_after(std::chrono::milliseconds(10)); + (void)co_await t.wait(); + auto [ec, n] = co_await s2.write_some( + capy::const_buffer(payload.data(), payload.size())); + (void)ec; + (void)n; + }; + + capy::run_async(ex)(reader()); + capy::run_async(ex)(writer()); + capy::run_async(ex)(peer_writer()); + ioc.run(); + + BOOST_TEST(!read_ec); + BOOST_TEST_EQ(read_n, payload.size()); + BOOST_TEST(!write_ec); + BOOST_TEST_EQ(write_n, payload.size()); + } + + // Park a wait_type::error op while a connect to an unreachable port + // is in progress. The EPOLLERR/SO_ERROR delivery triggers the + // wait_error_op completion path in descriptor_state's err block. + // Bounded by a fallback cancel so the wait doesn't hang if the + // backend does not surface the error as wait_type::error completion. + void testConnectErrorFiresWaitError() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + tcp_socket sock(ioc); + sock.open(); + + std::error_code conn_ec; + bool conn_done = false; + std::error_code wait_ec; + bool wait_done = false; + + auto connector = [&]() -> capy::task<> { + auto [ec] = co_await sock.connect( + endpoint(ipv4_address::loopback(), 1)); + conn_ec = ec; + conn_done = true; + }; + auto waiter = [&]() -> capy::task<> { + auto [ec] = co_await sock.wait(wait_type::error); + wait_ec = ec; + wait_done = true; + }; + auto canceller = [&]() -> capy::task<> { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(500)); + (void)co_await t.wait(); + sock.cancel(); + }; + + capy::run_async(ex)(connector()); + capy::run_async(ex)(waiter()); + capy::run_async(ex)(canceller()); + ioc.run(); + + BOOST_TEST(conn_done); + BOOST_TEST(conn_ec); + BOOST_TEST(wait_done); + } + + // Connect to an unreachable peer; the failed handshake delivers + // EPOLLERR. Exercises descriptor_state's error branch. + void testConnectFailureReportsError() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + tcp_socket sock(ioc); + sock.open(); + + // Use port 1 which is well-known reserved and very unlikely to + // be listening; the resulting connect will get RST or fail. + std::error_code conn_ec; + bool conn_done = false; + auto task = [&]() -> capy::task<> { + auto [ec] = co_await sock.connect( + endpoint(ipv4_address::loopback(), 1)); + conn_ec = ec; + conn_done = true; + }; + + capy::run_async(ex)(task()); + ioc.run(); + + BOOST_TEST(conn_done); + BOOST_TEST(conn_ec); + } + + // wait_type::error should complete when peer closes (reactor delivers + // HUP via the err/ready_events path). Bounded by a cancel timer because + // not every backend reports HUP as an error condition. + void testWaitForError() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + auto [s1, s2] = + test::make_socket_pair(ioc); + + std::error_code wait_ec; + bool wait_done = false; + + auto waiter = [&]() -> capy::task<> { + auto [ec] = co_await s1.wait(wait_type::error); + wait_ec = ec; + wait_done = true; + }; + auto closer = [&]() -> capy::task<> { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(20)); + (void)co_await t.wait(); + s2.close(); + // Bound the wait: cancel s1 after another delay if the peer + // close did not surface as an error condition. + timer t2(ioc); + t2.expires_after(std::chrono::milliseconds(200)); + (void)co_await t2.wait(); + s1.cancel(); + }; + + capy::run_async(ex)(waiter()); + capy::run_async(ex)(closer()); + ioc.run(); + + BOOST_TEST(wait_done); + // wait_ec may be: success (HUP), specific errno, or canceled — + // all valid termination conditions for a parked wait_error op. + } + + // Cancel a parked wait_type::error op via socket.cancel(). Exercises the + // wait_error_op cancellation path through reactor_basic_socket::do_cancel. + void testCancelWaitForError() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + auto [s1, s2] = + test::make_socket_pair(ioc); + + std::error_code wait_ec; + bool wait_done = false; + + auto waiter = [&]() -> capy::task<> { + auto [ec] = co_await s1.wait(wait_type::error); + wait_ec = ec; + wait_done = true; + }; + auto canceller = [&]() -> capy::task<> { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(20)); + (void)co_await t.wait(); + s1.cancel(); + }; + + capy::run_async(ex)(waiter()); + capy::run_async(ex)(canceller()); + ioc.run(); + + BOOST_TEST(wait_done); + BOOST_TEST(wait_ec == capy::cond::canceled); + } + + // Multi-buffer (scatter-gather) read. Exercises the readv() branch in + // reactor_stream_socket::do_read_some. + void testScatterRead() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + auto [s1, s2] = + test::make_socket_pair(ioc); + + constexpr std::string_view payload = "scatter-gather-payload-data"; + char buf1[8] = {}; + char buf2[8] = {}; + char buf3[32] = {}; + + std::error_code read_ec; + std::size_t read_n = 0; + + auto reader = [&]() -> capy::task<> { + std::array bufs = { + capy::mutable_buffer(buf1, sizeof(buf1)), + capy::mutable_buffer(buf2, sizeof(buf2)), + capy::mutable_buffer(buf3, sizeof(buf3)), + }; + auto [ec, n] = co_await s1.read_some(bufs); + read_ec = ec; + read_n = n; + }; + auto writer = [&]() -> capy::task<> { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(10)); + (void)co_await t.wait(); + auto [ec, n] = co_await s2.write_some( + capy::const_buffer(payload.data(), payload.size())); + (void)ec; + (void)n; + }; + + capy::run_async(ex)(reader()); + capy::run_async(ex)(writer()); + ioc.run(); + + BOOST_TEST(!read_ec); + BOOST_TEST_EQ(read_n, payload.size()); + } + + // Write a large payload into a socket with a tiny send buffer to + // force the EAGAIN path inside do_write_some, which parks the op in + // desc_state.write_op and re-arms via EPOLLOUT. + void testWriteEAGAIN() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + auto [s1, s2] = + test::make_socket_pair(ioc); + + // Shrink kernel buffers as much as possible. + s1.set_option(socket_option::send_buffer_size(1024)); + s2.set_option(socket_option::receive_buffer_size(1024)); + + constexpr std::size_t total = std::size_t{256} * 1024; // 256 KiB + std::vector payload(total, 'X'); + + std::error_code write_ec; + std::size_t bytes_written = 0; + std::error_code read_ec; + std::size_t bytes_read = 0; + + auto writer = [&]() -> capy::task<> { + std::size_t off = 0; + while (off < payload.size()) + { + auto [ec, n] = co_await s1.write_some( + capy::const_buffer( + payload.data() + off, payload.size() - off)); + if (ec) + { + write_ec = ec; + co_return; + } + off += n; + bytes_written = off; + } + }; + auto reader = [&]() -> capy::task<> { + std::vector buf(4096); + while (bytes_read < total) + { + auto [ec, n] = co_await s2.read_some( + capy::mutable_buffer(buf.data(), buf.size())); + if (ec) + { + read_ec = ec; + co_return; + } + if (n == 0) + co_return; + bytes_read += n; + } + }; + + capy::run_async(ex)(writer()); + capy::run_async(ex)(reader()); + ioc.run(); + + BOOST_TEST(!write_ec); + BOOST_TEST_EQ(bytes_written, total); + BOOST_TEST(!read_ec); + BOOST_TEST_EQ(bytes_read, total); + } + + // Multi-buffer (gather) write. Exercises the writev() branch in + // reactor_stream_socket::do_write_some. + void testGatherWrite() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + auto [s1, s2] = + test::make_socket_pair(ioc); + + constexpr std::string_view part1 = "hello-"; + constexpr std::string_view part2 = "scatter-"; + constexpr std::string_view part3 = "world"; + + std::error_code write_ec; + std::size_t write_n = 0; + char read_buf[64] = {}; + std::size_t total_read = 0; + std::error_code read_ec; + + auto writer = [&]() -> capy::task<> { + std::array bufs = { + capy::const_buffer(part1.data(), part1.size()), + capy::const_buffer(part2.data(), part2.size()), + capy::const_buffer(part3.data(), part3.size()), + }; + auto [ec, n] = co_await s1.write_some(bufs); + write_ec = ec; + write_n = n; + }; + auto reader = [&]() -> capy::task<> { + // Drain until we have the full expected payload or no more data. + std::size_t expected = part1.size() + part2.size() + part3.size(); + while (total_read < expected) + { + auto [ec, n] = co_await s2.read_some(capy::mutable_buffer( + read_buf + total_read, sizeof(read_buf) - total_read)); + if (ec) + { + read_ec = ec; + co_return; + } + if (n == 0) + co_return; + total_read += n; + } + }; + + capy::run_async(ex)(writer()); + capy::run_async(ex)(reader()); + ioc.run(); + + BOOST_TEST(!write_ec); + BOOST_TEST_EQ(write_n, part1.size() + part2.size() + part3.size()); + BOOST_TEST(!read_ec); + BOOST_TEST_EQ(total_read, part1.size() + part2.size() + part3.size()); + } + + // Acceptor wait_type::write completes immediately. Exercises the early + // return path in reactor_acceptor::do_wait. + void testAcceptorWaitWrite() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + tcp_acceptor acc(ioc); + acc.open(); + acc.set_option(socket_option::reuse_address(true)); + auto bec = acc.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!bec); + auto lec = acc.listen(); + BOOST_TEST(!lec); + + std::error_code wait_ec; + bool wait_done = false; + + auto waiter = [&]() -> capy::task<> { + auto [ec] = co_await acc.wait(wait_type::write); + wait_ec = ec; + wait_done = true; + }; + + capy::run_async(ex)(waiter()); + ioc.run(); + + BOOST_TEST(wait_done); + BOOST_TEST(!wait_ec); + } + + // Cancel a parked acceptor wait_type::error. Exercises the + // wait_er_ cancel path in reactor_acceptor::do_cancel. + void testAcceptorWaitErrorCancelled() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + tcp_acceptor acc(ioc); + acc.open(); + acc.set_option(socket_option::reuse_address(true)); + auto bec = acc.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!bec); + auto lec = acc.listen(); + BOOST_TEST(!lec); + + std::error_code wait_ec; + bool wait_done = false; + + auto waiter = [&]() -> capy::task<> { + auto [ec] = co_await acc.wait(wait_type::error); + wait_ec = ec; + wait_done = true; + }; + auto canceller = [&]() -> capy::task<> { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(20)); + (void)co_await t.wait(); + acc.cancel(); + }; + + capy::run_async(ex)(waiter()); + capy::run_async(ex)(canceller()); + ioc.run(); + + BOOST_TEST(wait_done); + BOOST_TEST(wait_ec == capy::cond::canceled); + } + + // UDP wait_type::write completes immediately. + void testUdpWaitWrite() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + udp_socket sock(ioc); + sock.open(udp::v4()); + auto bec = sock.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!bec); + + std::error_code wait_ec; + bool wait_done = false; + + auto waiter = [&]() -> capy::task<> { + auto [ec] = co_await sock.wait(wait_type::write); + wait_ec = ec; + wait_done = true; + }; + + capy::run_async(ex)(waiter()); + ioc.run(); + + BOOST_TEST(wait_done); + BOOST_TEST(!wait_ec); + } + + // UDP wait_type::error then cancel. Exercises wait_er_ cancel path + // on reactor_datagram_socket. + void testUdpWaitErrorCancel() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + udp_socket sock(ioc); + sock.open(udp::v4()); + auto bec = sock.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!bec); + + std::error_code wait_ec; + bool wait_done = false; + + auto waiter = [&]() -> capy::task<> { + auto [ec] = co_await sock.wait(wait_type::error); + wait_ec = ec; + wait_done = true; + }; + auto canceller = [&]() -> capy::task<> { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(20)); + (void)co_await t.wait(); + sock.cancel(); + }; + + capy::run_async(ex)(waiter()); + capy::run_async(ex)(canceller()); + ioc.run(); + + BOOST_TEST(wait_done); + BOOST_TEST(wait_ec == capy::cond::canceled); + } + + // UDP recv_from with a zero-length buffer should complete immediately + // with zero bytes. Exercises the empty-buffer branch in do_recv_from. + void testUdpRecvFromEmpty() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + udp_socket sock(ioc); + sock.open(udp::v4()); + auto bec = sock.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!bec); + + std::error_code rf_ec; + std::size_t rf_n = 1; + endpoint src; + + auto reader = [&]() -> capy::task<> { + auto [ec, n] = co_await sock.recv_from( + capy::mutable_buffer(nullptr, 0), src); + rf_ec = ec; + rf_n = n; + }; + + capy::run_async(ex)(reader()); + ioc.run(); + + BOOST_TEST(!rf_ec); + BOOST_TEST_EQ(rf_n, 0u); + } + + // UDP send with a zero-length buffer should complete immediately. + void testUdpSendEmpty() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + // Use a connected pair to avoid sendto address issues. + udp_socket s1(ioc); + udp_socket s2(ioc); + s1.open(udp::v4()); + s2.open(udp::v4()); + auto e1 = s1.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!e1); + auto e2 = s2.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!e2); + auto port = s2.local_endpoint().port(); + + std::error_code conn_ec; + bool conn_done = false; + auto connect_task = [&]() -> capy::task<> { + auto [ec] = co_await s1.connect( + endpoint(ipv4_address::loopback(), port)); + conn_ec = ec; + conn_done = true; + }; + capy::run_async(ex)(connect_task()); + ioc.run(); + ioc.restart(); + BOOST_TEST(conn_done); + BOOST_TEST(!conn_ec); + + std::error_code send_ec; + std::size_t send_n = 1; + + auto sender = [&]() -> capy::task<> { + auto [ec, n] = + co_await s1.send(capy::const_buffer(nullptr, 0)); + send_ec = ec; + send_n = n; + }; + + capy::run_async(ex)(sender()); + ioc.run(); + + // Zero-length UDP send: Linux accepts; macOS returns EMSGSIZE; + // various Windows variants return EINVAL or other errnos. The + // library send path is exercised regardless. + if (!send_ec) + BOOST_TEST_EQ(send_n, 0u); + } + + // Connected UDP recv with zero-length buffer (do_recv empty path). + void testUdpRecvEmpty() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + udp_socket s1(ioc); + udp_socket s2(ioc); + s1.open(udp::v4()); + s2.open(udp::v4()); + auto e1 = s1.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!e1); + auto e2 = s2.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!e2); + auto port = s2.local_endpoint().port(); + + std::error_code conn_ec; + bool conn_done = false; + auto connect_task = [&]() -> capy::task<> { + auto [ec] = co_await s1.connect( + endpoint(ipv4_address::loopback(), port)); + conn_ec = ec; + conn_done = true; + }; + capy::run_async(ex)(connect_task()); + ioc.run(); + ioc.restart(); + BOOST_TEST(conn_done); + BOOST_TEST(!conn_ec); + + std::error_code recv_ec; + std::size_t recv_n = 1; + + auto receiver = [&]() -> capy::task<> { + auto [ec, n] = co_await s1.recv(capy::mutable_buffer(nullptr, 0)); + recv_ec = ec; + recv_n = n; + }; + + capy::run_async(ex)(receiver()); + ioc.run(); + + BOOST_TEST(!recv_ec); + BOOST_TEST_EQ(recv_n, 0u); + } + + // Connected UDP send_to with proper endpoint should still parse correctly. + void testUdpSendToEmpty() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + udp_socket s1(ioc); + udp_socket s2(ioc); + s1.open(udp::v4()); + s2.open(udp::v4()); + auto e1 = s1.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!e1); + auto e2 = s2.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!e2); + auto port = s2.local_endpoint().port(); + + std::error_code send_ec; + std::size_t send_n = 1; + + auto sender = [&]() -> capy::task<> { + auto [ec, n] = co_await s1.send_to( + capy::const_buffer(nullptr, 0), + endpoint(ipv4_address::loopback(), port)); + send_ec = ec; + send_n = n; + }; + + capy::run_async(ex)(sender()); + ioc.run(); + + // Zero-length UDP send_to: same platform variation as testUdpSendEmpty. + // The library send_to path is exercised regardless of the + // platform's specific errno. + if (!send_ec) + BOOST_TEST_EQ(send_n, 0u); + } + + // Shutdown both directions on a stream socket. + void testShutdownBoth() + { + io_context ioc(Backend); + auto [s1, s2] = + test::make_socket_pair(ioc); + + bool ok = false; + auto task = [&]() -> capy::task<> { + s1.shutdown(shutdown_both); + ok = true; + co_return; + }; + capy::run_async(ioc.get_executor())(task()); + ioc.run(); + BOOST_TEST(ok); + } + + // Shutdown receive direction. + void testShutdownReceive() + { + io_context ioc(Backend); + auto [s1, s2] = + test::make_socket_pair(ioc); + + bool ok = false; + auto task = [&]() -> capy::task<> { + s1.shutdown(shutdown_receive); + ok = true; + co_return; + }; + capy::run_async(ioc.get_executor())(task()); + ioc.run(); + BOOST_TEST(ok); + } + + // Close a UDP socket with a parked wait. Exercises the close path's + // claim-and-repost over wait_er_/wait_rd_/wait_wr_ slots. + void testUdpCloseWhileWaiting() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + udp_socket sock(ioc); + sock.open(udp::v4()); + auto bec = sock.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!bec); + + std::error_code wait_ec; + bool wait_done = false; + + auto waiter = [&]() -> capy::task<> { + auto [ec] = co_await sock.wait(wait_type::read); + wait_ec = ec; + wait_done = true; + }; + auto closer = [&]() -> capy::task<> { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(20)); + (void)co_await t.wait(); + sock.close(); + }; + + capy::run_async(ex)(waiter()); + capy::run_async(ex)(closer()); + ioc.run(); + + BOOST_TEST(wait_done); + BOOST_TEST(wait_ec == capy::cond::canceled); + } + + // Multiple acceptors on the same scheduler accept concurrently. + // Exercises reactor's multi-fd registration and dispatch path. + void testMultipleAcceptors() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + constexpr int N = 3; + std::vector accs; + std::vector peers; + std::vector clients; + std::vector ports; + accs.reserve(N); + peers.reserve(N); + clients.reserve(N); + ports.reserve(N); + + for (int i = 0; i < N; ++i) + { + accs.emplace_back(ioc); + accs.back().open(); + accs.back().set_option(socket_option::reuse_address(true)); + BOOST_TEST(!accs.back().bind(endpoint(ipv4_address::loopback(), 0))); + BOOST_TEST(!accs.back().listen()); + ports.push_back(accs.back().local_endpoint().port()); + peers.emplace_back(ioc); + clients.emplace_back(ioc); + clients.back().open(); + } + + std::array accept_done{}; + std::array connect_done{}; + + for (int i = 0; i < N; ++i) + { + capy::run_async(ex)( + [](tcp_acceptor& a, tcp_socket& p, bool& done) + -> capy::task<> { + auto [ec] = co_await a.accept(p); + (void)ec; + done = true; + }(accs[i], peers[i], accept_done[i])); + capy::run_async(ex)( + [](tcp_socket& c, endpoint ep, bool& done) -> capy::task<> { + auto [ec] = co_await c.connect(ep); + (void)ec; + done = true; + }(clients[i], endpoint(ipv4_address::loopback(), ports[i]), + connect_done[i])); + } + + ioc.run(); + + for (int i = 0; i < N; ++i) + { + BOOST_TEST(accept_done[i]); + BOOST_TEST(connect_done[i]); + } + } + + // Stop-token cancel of a parked wait(read). Exercises cancel_single_op + // via the per-op canceller (not socket.cancel()), which dispatches + // through op_to_desc_slot/op_to_cancel_flag for the wait_rd_ slot. + void testStopTokenWaitRead() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + auto [s1, s2] = + test::make_socket_pair(ioc); + + std::stop_source ss; + std::error_code wait_ec; + bool wait_done = false; + + auto waiter = [&]() -> capy::task<> { + auto [ec] = co_await s1.wait(wait_type::read); + wait_ec = ec; + wait_done = true; + }; + auto canceller = [&]() -> capy::task<> { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(20)); + (void)co_await t.wait(); + ss.request_stop(); + }; + + capy::run_async(ex, ss.get_token())(waiter()); + capy::run_async(ex)(canceller()); + ioc.run(); + + BOOST_TEST(wait_done); + BOOST_TEST(wait_ec == capy::cond::canceled); + } + + // Stop-token cancel of wait(error). + void testStopTokenWaitError() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + auto [s1, s2] = + test::make_socket_pair(ioc); + + std::stop_source ss; + std::error_code wait_ec; + bool wait_done = false; + + auto waiter = [&]() -> capy::task<> { + auto [ec] = co_await s1.wait(wait_type::error); + wait_ec = ec; + wait_done = true; + }; + auto canceller = [&]() -> capy::task<> { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(20)); + (void)co_await t.wait(); + ss.request_stop(); + }; + + capy::run_async(ex, ss.get_token())(waiter()); + capy::run_async(ex)(canceller()); + ioc.run(); + + BOOST_TEST(wait_done); + BOOST_TEST(wait_ec == capy::cond::canceled); + } + + // Stop-token cancel of a UDP recv. Exercises cancel_single_op on the + // recv_rd_ slot in reactor_datagram_socket. + void testStopTokenUdpRecv() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + udp_socket s1(ioc); + udp_socket s2(ioc); + s1.open(udp::v4()); + s2.open(udp::v4()); + auto e1 = s1.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!e1); + auto e2 = s2.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!e2); + auto port = s2.local_endpoint().port(); + + std::error_code conn_ec; + bool conn_done = false; + auto connect_task = [&]() -> capy::task<> { + auto [ec] = co_await s1.connect( + endpoint(ipv4_address::loopback(), port)); + conn_ec = ec; + conn_done = true; + }; + capy::run_async(ex)(connect_task()); + ioc.run(); + ioc.restart(); + BOOST_TEST(conn_done); + BOOST_TEST(!conn_ec); + + std::stop_source ss; + std::error_code recv_ec; + bool recv_done = false; + char buf[64]; + + auto receiver = [&]() -> capy::task<> { + auto [ec, n] = + co_await s1.recv(capy::mutable_buffer(buf, sizeof(buf))); + (void)n; + recv_ec = ec; + recv_done = true; + }; + auto canceller = [&]() -> capy::task<> { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(20)); + (void)co_await t.wait(); + ss.request_stop(); + }; + + capy::run_async(ex, ss.get_token())(receiver()); + capy::run_async(ex)(canceller()); + ioc.run(); + + BOOST_TEST(recv_done); + BOOST_TEST(recv_ec == capy::cond::canceled); + } + + // Stop-token cancel of a UDP recv_from. + void testStopTokenUdpRecvFrom() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + udp_socket sock(ioc); + sock.open(udp::v4()); + auto bec = sock.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!bec); + + std::stop_source ss; + std::error_code recv_ec; + bool recv_done = false; + endpoint src; + char buf[64]; + + auto receiver = [&]() -> capy::task<> { + auto [ec, n] = co_await sock.recv_from( + capy::mutable_buffer(buf, sizeof(buf)), src); + (void)n; + recv_ec = ec; + recv_done = true; + }; + auto canceller = [&]() -> capy::task<> { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(20)); + (void)co_await t.wait(); + ss.request_stop(); + }; + + capy::run_async(ex, ss.get_token())(receiver()); + capy::run_async(ex)(canceller()); + ioc.run(); + + BOOST_TEST(recv_done); + BOOST_TEST(recv_ec == capy::cond::canceled); + } + + // Stop-token cancel of an acceptor accept(). + void testStopTokenAcceptorAccept() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + tcp_acceptor acc(ioc); + acc.open(); + acc.set_option(socket_option::reuse_address(true)); + BOOST_TEST(!acc.bind(endpoint(ipv4_address::loopback(), 0))); + BOOST_TEST(!acc.listen()); + + std::stop_source ss; + std::error_code accept_ec; + bool accept_done = false; + tcp_socket peer(ioc); + + auto acceptor_task = [&]() -> capy::task<> { + auto [ec] = co_await acc.accept(peer); + accept_ec = ec; + accept_done = true; + }; + auto canceller = [&]() -> capy::task<> { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(20)); + (void)co_await t.wait(); + ss.request_stop(); + }; + + capy::run_async(ex, ss.get_token())(acceptor_task()); + capy::run_async(ex)(canceller()); + ioc.run(); + + BOOST_TEST(accept_done); + BOOST_TEST(accept_ec == capy::cond::canceled); + } + + // configure_reactor with max_events == 0 throws std::out_of_range. + // IOCP ignores max_events_per_poll (no batch poll), so only test + // on reactor backends. + void testIoContextOptionsMaxEventsZero() + { +#if BOOST_COROSIO_POSIX + io_context_options opts; + opts.max_events_per_poll = 0; + bool threw = false; + try + { + io_context ioc(Backend, opts); + (void)ioc; + } + catch (std::out_of_range const&) + { + threw = true; + } + BOOST_TEST(threw); +#endif + } + + // configure_reactor with budget_init > budget_max clamps. + void testIoContextOptionsBudgetInitClamp() + { + io_context_options opts; + opts.inline_budget_initial = 100; + opts.inline_budget_max = 5; + opts.unassisted_budget = 200; + io_context ioc(Backend, opts); + // Construction succeeds; values are silently clamped. + BOOST_TEST(true); + } + +#if BOOST_COROSIO_POSIX + // Assign a non-AF_UNIX fd to a local_stream_socket: validation + // returns EAFNOSUPPORT (do_assign_fd::ss_family != AF_UNIX path). + void testLocalStreamAssignWrongFamily() + { + io_context ioc(Backend); + + // Create a TCP socket fd that's not AF_UNIX. + int tcp_fd = ::socket(AF_INET, SOCK_STREAM, 0); + BOOST_TEST(tcp_fd >= 0); + + local_stream_socket sock(ioc); + bool threw = false; + try + { + sock.assign(tcp_fd); + } + catch (std::system_error const&) + { + threw = true; + } + // assign may throw on wrong type/family; cleanup if owned. + if (!sock.is_open()) + ::close(tcp_fd); + BOOST_TEST(threw); + } + + // Assign a stream-type fd (AF_UNIX SOCK_STREAM) to local_datagram_socket: + // validation returns EPROTOTYPE. + void testLocalDgramAssignWrongType() + { + io_context ioc(Backend); + + int fd = ::socket(AF_UNIX, SOCK_STREAM, 0); + BOOST_TEST(fd >= 0); + + local_datagram_socket sock(ioc); + bool threw = false; + try + { + sock.assign(fd); + } + catch (std::system_error const&) + { + threw = true; + } + if (!sock.is_open()) + ::close(fd); + BOOST_TEST(threw); + } +#endif + + // Issue wait() with an already-stopped token. Exercises the + // op.cancelled.load() == true branch in reactor_acceptor::do_wait. + void testWaitWithPreCancelledToken() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + + tcp_acceptor acc(ioc); + acc.open(); + acc.set_option(socket_option::reuse_address(true)); + BOOST_TEST(!acc.bind(endpoint(ipv4_address::loopback(), 0))); + BOOST_TEST(!acc.listen()); + + std::stop_source ss; + ss.request_stop(); // Already stopped before the wait registers. + std::error_code wait_ec; + bool wait_done = false; + + auto waiter = [&]() -> capy::task<> { + auto [ec] = co_await acc.wait(wait_type::read); + wait_ec = ec; + wait_done = true; + }; + + capy::run_async(ex, ss.get_token())(waiter()); + ioc.run(); + + BOOST_TEST(wait_done); + BOOST_TEST(wait_ec == capy::cond::canceled); + } + +#if BOOST_COROSIO_POSIX + // Local stream socket wait_type::error then cancel. Exercises the + // local-endpoint specialization of reactor_stream_socket. + void testLocalStreamWaitErrorCancel() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + local_stream_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + std::error_code wait_ec; + bool wait_done = false; + + auto waiter = [&]() -> capy::task<> { + auto [ec] = co_await s1.wait(wait_type::error); + wait_ec = ec; + wait_done = true; + }; + auto canceller = [&]() -> capy::task<> { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(20)); + (void)co_await t.wait(); + s1.cancel(); + }; + + capy::run_async(ex)(waiter()); + capy::run_async(ex)(canceller()); + ioc.run(); + + BOOST_TEST(wait_done); + BOOST_TEST(wait_ec == capy::cond::canceled); + } + + // Local stream socket wait_type::write completes immediately. + void testLocalStreamWaitWrite() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + local_stream_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + std::error_code wait_ec; + bool wait_done = false; + + auto waiter = [&]() -> capy::task<> { + auto [ec] = co_await s1.wait(wait_type::write); + wait_ec = ec; + wait_done = true; + }; + + capy::run_async(ex)(waiter()); + ioc.run(); + + BOOST_TEST(wait_done); + BOOST_TEST(!wait_ec); + } + + // Local stream scatter-gather read. + void testLocalStreamScatterRead() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + local_stream_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + constexpr std::string_view payload = "local-scatter-read-payload"; + char a[6] = {}; + char b[6] = {}; + char c[64] = {}; + + std::error_code read_ec; + std::size_t read_n = 0; + + auto reader = [&]() -> capy::task<> { + std::array bufs = { + capy::mutable_buffer(a, sizeof(a)), + capy::mutable_buffer(b, sizeof(b)), + capy::mutable_buffer(c, sizeof(c)), + }; + auto [ec, n] = co_await s1.read_some(bufs); + read_ec = ec; + read_n = n; + }; + auto writer = [&]() -> capy::task<> { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(10)); + (void)co_await t.wait(); + auto [ec, n] = co_await s2.write_some( + capy::const_buffer(payload.data(), payload.size())); + (void)ec; + (void)n; + }; + + capy::run_async(ex)(reader()); + capy::run_async(ex)(writer()); + ioc.run(); + + BOOST_TEST(!read_ec); + BOOST_TEST_EQ(read_n, payload.size()); + } + + // Local stream gather write. + void testLocalStreamGatherWrite() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + local_stream_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + constexpr std::string_view part1 = "aaa-"; + constexpr std::string_view part2 = "bbb-"; + constexpr std::string_view part3 = "ccc"; + + std::error_code write_ec; + std::size_t write_n = 0; + + auto writer = [&]() -> capy::task<> { + std::array bufs = { + capy::const_buffer(part1.data(), part1.size()), + capy::const_buffer(part2.data(), part2.size()), + capy::const_buffer(part3.data(), part3.size()), + }; + auto [ec, n] = co_await s1.write_some(bufs); + write_ec = ec; + write_n = n; + }; + auto reader = [&]() -> capy::task<> { + char buf[64]; + auto [ec, n] = + co_await s2.read_some(capy::mutable_buffer(buf, sizeof(buf))); + (void)ec; + (void)n; + }; + + capy::run_async(ex)(writer()); + capy::run_async(ex)(reader()); + ioc.run(); + + BOOST_TEST(!write_ec); + BOOST_TEST_EQ(write_n, part1.size() + part2.size() + part3.size()); + } + + // Local datagram socket wait_type::error then cancel. + void testLocalDgramWaitErrorCancel() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + local_datagram_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + std::error_code wait_ec; + bool wait_done = false; + + auto waiter = [&]() -> capy::task<> { + auto [ec] = co_await s1.wait(wait_type::error); + wait_ec = ec; + wait_done = true; + }; + auto canceller = [&]() -> capy::task<> { + timer t(ioc); + t.expires_after(std::chrono::milliseconds(20)); + (void)co_await t.wait(); + s1.cancel(); + }; + + capy::run_async(ex)(waiter()); + capy::run_async(ex)(canceller()); + ioc.run(); + + BOOST_TEST(wait_done); + BOOST_TEST(wait_ec == capy::cond::canceled); + } + + // Local datagram wait_type::write immediate completion. + void testLocalDgramWaitWrite() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + local_datagram_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + std::error_code wait_ec; + bool wait_done = false; + + auto waiter = [&]() -> capy::task<> { + auto [ec] = co_await s1.wait(wait_type::write); + wait_ec = ec; + wait_done = true; + }; + + capy::run_async(ex)(waiter()); + ioc.run(); + + BOOST_TEST(wait_done); + BOOST_TEST(!wait_ec); + } + + // Local datagram recv with zero-length buffer. + void testLocalDgramRecvEmpty() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + local_datagram_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + std::error_code recv_ec; + std::size_t recv_n = 1; + + auto receiver = [&]() -> capy::task<> { + auto [ec, n] = co_await s1.recv(capy::mutable_buffer(nullptr, 0)); + recv_ec = ec; + recv_n = n; + }; + + capy::run_async(ex)(receiver()); + ioc.run(); + + BOOST_TEST(!recv_ec); + BOOST_TEST_EQ(recv_n, 0u); + } + + // Local datagram send with zero-length buffer (connected). + void testLocalDgramSendEmpty() + { + io_context ioc(Backend); + auto ex = ioc.get_executor(); + local_datagram_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + std::error_code send_ec; + std::size_t send_n = 1; + + auto sender = [&]() -> capy::task<> { + auto [ec, n] = co_await s1.send(capy::const_buffer(nullptr, 0)); + send_ec = ec; + send_n = n; + }; + + capy::run_async(ex)(sender()); + ioc.run(); + + // Zero-length local datagram send: Linux accepts; macOS returns + // EMSGSIZE; other platforms vary. The library send path is + // exercised regardless. + if (!send_ec) + BOOST_TEST_EQ(send_n, 0u); + } + + // Local datagram socket shutdown_both. + void testLocalDgramShutdownBoth() + { + io_context ioc(Backend); + local_datagram_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + std::error_code ec; + auto task = [&]() -> capy::task<> { + s1.shutdown(shutdown_both, ec); + co_return; + }; + capy::run_async(ioc.get_executor())(task()); + ioc.run(); + BOOST_TEST(!ec); + } + + // Local datagram socket shutdown_receive. + void testLocalDgramShutdownReceive() + { + io_context ioc(Backend); + local_datagram_socket s1(ioc), s2(ioc); + if (auto ec = connect_pair(s1, s2)) + throw std::system_error(ec, "connect_pair"); + + std::error_code ec; + auto task = [&]() -> capy::task<> { + s1.shutdown(shutdown_receive, ec); + co_return; + }; + capy::run_async(ioc.get_executor())(task()); + ioc.run(); + BOOST_TEST(!ec); + } +#endif + + void run() + { + testConcurrentReadWrite(); + testConnectFailureReportsError(); + testConnectErrorFiresWaitError(); + testWaitForError(); + testCancelWaitForError(); + testScatterRead(); + testWriteEAGAIN(); + testGatherWrite(); + testAcceptorWaitWrite(); + testAcceptorWaitErrorCancelled(); + testUdpWaitWrite(); + testUdpWaitErrorCancel(); + testUdpRecvFromEmpty(); + testUdpSendEmpty(); + testUdpRecvEmpty(); + testUdpSendToEmpty(); + testShutdownBoth(); + testShutdownReceive(); + testUdpCloseWhileWaiting(); + testMultipleAcceptors(); + testStopTokenWaitRead(); + testStopTokenWaitError(); + testStopTokenUdpRecv(); + testStopTokenUdpRecvFrom(); + testStopTokenAcceptorAccept(); + testIoContextOptionsMaxEventsZero(); + testIoContextOptionsBudgetInitClamp(); +#if BOOST_COROSIO_POSIX + testLocalStreamAssignWrongFamily(); + testLocalDgramAssignWrongType(); +#endif + testWaitWithPreCancelledToken(); +#if BOOST_COROSIO_POSIX + testLocalStreamWaitErrorCancel(); + testLocalStreamWaitWrite(); + testLocalStreamScatterRead(); + testLocalStreamGatherWrite(); + testLocalDgramWaitErrorCancel(); + testLocalDgramWaitWrite(); + testLocalDgramRecvEmpty(); + testLocalDgramSendEmpty(); + testLocalDgramShutdownBoth(); + testLocalDgramShutdownReceive(); +#endif + } +}; + +COROSIO_BACKEND_TESTS(reactor_paths_test, "boost.corosio.reactor_paths") + +} // namespace boost::corosio diff --git a/test/unit/resolver.cpp b/test/unit/resolver.cpp index 2ccc83bfc..f0cbb7a59 100644 --- a/test/unit/resolver.cpp +++ b/test/unit/resolver.cpp @@ -18,9 +18,12 @@ #include #include +#include #include #include +#include + #include "test_suite.hpp" namespace boost::corosio { @@ -324,6 +327,138 @@ struct resolver_test BOOST_TEST(result_ec); // Should have an error } + void testResolveSingleThreadedNotSupported() + { + // Single-threaded contexts disable the resolver thread pool and + // surface operation_not_supported instead of dispatching work. + io_context ioc(1); + resolver r(ioc); + + bool completed = false; + std::error_code result_ec; + + auto task = [](resolver& r_ref, + std::error_code& ec_out, bool& done) -> capy::task<> { + auto [ec, res] = co_await r_ref.resolve("localhost", "80"); + ec_out = ec; + done = true; + (void)res; + }; + capy::run_async(ioc.get_executor())(task(r, result_ec, completed)); + ioc.run(); + + BOOST_TEST(completed); + // Reactor backends disable the resolver thread pool in + // single-threaded mode and return operation_not_supported. + // IOCP uses an async native resolver that works regardless. +#if BOOST_COROSIO_POSIX + BOOST_TEST(result_ec == std::errc::operation_not_supported); +#else + BOOST_TEST(!result_ec); +#endif + } + + void testReverseResolveSingleThreadedNotSupported() + { + io_context ioc(1); + resolver r(ioc); + + bool completed = false; + std::error_code result_ec; + + auto task = [](resolver& r_ref, + std::error_code& ec_out, bool& done) -> capy::task<> { + endpoint ep(ipv4_address({127, 0, 0, 1}), 80); + auto [ec, res] = co_await r_ref.resolve(ep); + ec_out = ec; + done = true; + (void)res; + }; + capy::run_async(ioc.get_executor())(task(r, result_ec, completed)); + ioc.run(); + + BOOST_TEST(completed); +#if BOOST_COROSIO_POSIX + BOOST_TEST(result_ec == std::errc::operation_not_supported); +#else + BOOST_TEST(!result_ec); +#endif + } + + void testResolveInvalidFlagsCombination() + { + // numeric_service with a non-numeric service should produce EAI_NONAME + // or similar — exercises the make_gai_error mapping path. + io_context ioc; + resolver r(ioc); + + bool completed = false; + std::error_code result_ec; + + auto task = [](resolver& r_ref, + std::error_code& ec_out, bool& done) -> capy::task<> { + auto [ec, res] = co_await r_ref.resolve( + "127.0.0.1", "not-a-real-service", + resolve_flags::numeric_host | resolve_flags::numeric_service); + ec_out = ec; + done = true; + (void)res; + }; + capy::run_async(ioc.get_executor())(task(r, result_ec, completed)); + ioc.run(); + + BOOST_TEST(completed); + BOOST_TEST(result_ec); + } + + void testResolveWithVariedFlags() + { + // Exercise flags_to_hints for AI_PASSIVE / AI_ADDRCONFIG / AI_V4MAPPED + // / AI_ALL paths. Resolution itself need not succeed; the only + // requirement is the flag mapping be reached. + io_context ioc; + resolver r(ioc); + + auto task = [](resolver& r_ref) -> capy::task<> { + auto flags = resolve_flags::passive | + resolve_flags::address_configured | + resolve_flags::v4_mapped | resolve_flags::all_matching; + auto [ec, res] = co_await r_ref.resolve("127.0.0.1", "80", flags); + (void)ec; + (void)res; + }; + capy::run_async(ioc.get_executor())(task(r)); + ioc.run(); + BOOST_TEST_PASS(); + } + + void testReverseResolveDatagramFlag() + { + // Exercises flags_to_ni_flags NI_DGRAM branch. + io_context ioc; + resolver r(ioc); + + std::error_code result_ec; + reverse_resolver_result result; + + auto task = [](resolver& r_ref, std::error_code& ec_out, + reverse_resolver_result& res_out) -> capy::task<> { + endpoint ep(ipv4_address({127, 0, 0, 1}), 53); + auto [ec, res] = co_await r_ref.resolve( + ep, + reverse_flags::numeric_host | + reverse_flags::numeric_service | + reverse_flags::datagram_service); + ec_out = ec; + res_out = std::move(res); + }; + capy::run_async(ioc.get_executor())(task(r, result_ec, result)); + ioc.run(); + + BOOST_TEST(!result_ec); + BOOST_TEST_EQ(result.host_name(), "127.0.0.1"); + } + // Cancellation tests void testCancel() @@ -367,6 +502,66 @@ struct resolver_test BOOST_TEST_PASS(); } + void testResolveStopTokenCancellation() + { + // Pre-stopped token: the stop_callback fires inside start() + // and routes through the canceller's operator()() path. + io_context ioc; + resolver r(ioc); + + std::stop_source stop_src; + stop_src.request_stop(); + + bool completed = false; + std::error_code result_ec; + + auto task = [](resolver& r_ref, std::error_code& ec_out, + bool& done) -> capy::task<> { + auto [ec, res] = co_await r_ref.resolve("localhost", "80"); + ec_out = ec; + done = true; + (void)res; + }; + capy::run_async(ioc.get_executor(), stop_src.get_token())( + task(r, result_ec, completed)); + + ioc.run(); + + BOOST_TEST(completed); + // The token may be observed either by the stop_callback + // (canceller path) or via the worker's cancelled check — + // either way we get a canceled error code. + BOOST_TEST(result_ec == capy::cond::canceled); + } + + void testReverseResolveStopTokenCancellation() + { + io_context ioc; + resolver r(ioc); + + std::stop_source stop_src; + stop_src.request_stop(); + + bool completed = false; + std::error_code result_ec; + + auto task = [](resolver& r_ref, std::error_code& ec_out, + bool& done) -> capy::task<> { + endpoint ep(ipv4_address({127, 0, 0, 1}), 80); + auto [ec, res] = co_await r_ref.resolve(ep); + ec_out = ec; + done = true; + (void)res; + }; + capy::run_async(ioc.get_executor(), stop_src.get_token())( + task(r, result_ec, completed)); + + ioc.run(); + + BOOST_TEST(completed); + BOOST_TEST(result_ec == capy::cond::canceled); + } + // Sequential resolution tests void testSequentialResolves() @@ -882,10 +1077,17 @@ struct resolver_test // Error handling testResolveInvalidHost(); testResolveInvalidNumericHost(); + testResolveInvalidFlagsCombination(); + testResolveWithVariedFlags(); + testResolveSingleThreadedNotSupported(); + testReverseResolveSingleThreadedNotSupported(); + testReverseResolveDatagramFlag(); // Cancellation testCancel(); testCancelNoOperation(); + testResolveStopTokenCancellation(); + testReverseResolveStopTokenCancellation(); // Sequential resolves testSequentialResolves(); diff --git a/test/unit/signal_set.cpp b/test/unit/signal_set.cpp index 1c0e68152..ea8ed9ac6 100644 --- a/test/unit/signal_set.cpp +++ b/test/unit/signal_set.cpp @@ -127,6 +127,26 @@ struct signal_set_test BOOST_TEST(!!result); } + void testAddInvalidLargeSignal() + { + io_context ioc(Backend); + signal_set s(ioc); + + // A signal number above the service's table size returns + // invalid_argument. + auto result = s.add(100000); + BOOST_TEST(!!result); + } + + void testRemoveInvalidSignal() + { + io_context ioc(Backend); + signal_set s(ioc); + + auto result = s.remove(-1); + BOOST_TEST(!!result); + } + void testRemove() { io_context ioc(Backend); @@ -310,6 +330,51 @@ struct signal_set_test BOOST_TEST_PASS(); } + void testWaitWithPreStoppedToken() + { + // A wait() under a stop_token that's already in the requested state + // completes with capy::error::canceled via the stop_requested branch. + io_context ioc(Backend); + signal_set s(ioc, SIGINT); + + std::stop_source src; + src.request_stop(); + + bool completed = false; + std::error_code result_ec; + + auto wait_task = [&]() -> capy::task<> { + auto [ec, signum] = co_await s.wait(); + result_ec = ec; + completed = true; + (void)signum; + }; + capy::run_async(ioc.get_executor(), src.get_token())(wait_task()); + + ioc.run(); + BOOST_TEST(completed); + BOOST_TEST(result_ec == capy::cond::canceled); + } + + void testShutdownWithPendingSignalSet() + { + // Construct a signal_set that owns a signal registration, then let + // the io_context shutdown drain the impl_list (covers shutdown + // loop deleting registrations). + int destroyed = 0; + (void)destroyed; + + { + io_context ioc(Backend); + signal_set s(ioc, SIGINT, SIGTERM); + (void)s; + // No run() — drop directly into io_context destruction so the + // service's shutdown path walks impl_list_ and frees both + // signal_registration nodes. + } + BOOST_TEST_PASS(); + } + // Multiple signal set tests void testMultipleSignalSetsOnSameSignal() @@ -738,6 +803,8 @@ struct signal_set_test testAdd(); testAddDuplicate(); testAddInvalidSignal(); + testAddInvalidLargeSignal(); + testRemoveInvalidSignal(); testRemove(); testRemoveNotPresent(); testClear(); @@ -752,6 +819,8 @@ struct signal_set_test testCancelBeforeWait(); testCancelNoWaiters(); testCancelMultipleTimes(); + testWaitWithPreStoppedToken(); + testShutdownWithPendingSignalSet(); // Multiple signal set tests testMultipleSignalSetsOnSameSignal(); diff --git a/test/unit/stream_file.cpp b/test/unit/stream_file.cpp index 3fdd84604..2b8fa6d22 100644 --- a/test/unit/stream_file.cpp +++ b/test/unit/stream_file.cpp @@ -185,6 +185,31 @@ struct stream_file_test BOOST_TEST(threw); } + void testOpenSyncAllOnWrite() + { + // Exercises the O_SYNC mapping in posix_stream_file::open_file. + temp_file tmp("sf_sync_open_"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, + file_base::write_only | file_base::create + | file_base::truncate | file_base::sync_all_on_write); + BOOST_TEST(f.is_open()); + + bool done = false; + auto task = [](stream_file& f_ref, bool& d) -> capy::task<> { + auto [ec, n] = + co_await f_ref.write_some(capy::const_buffer("synced", 6)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 6u); + d = true; + }; + capy::run_async(ioc.get_executor())(task(f, done)); + ioc.run(); + BOOST_TEST(done); + } + // File metadata void testSize() @@ -422,6 +447,119 @@ struct stream_file_test BOOST_TEST_PASS(); } + void testCancelOnClosedFile() + { + io_context ioc; + stream_file f(ioc); + + // cancel() on a closed file is a no-op (early return). + f.cancel(); + BOOST_TEST(!f.is_open()); + } + + void testNativeHandleClosedAndOpen() + { + temp_file tmp("sf_nh_", "x"); + io_context ioc; + stream_file f(ioc); + +#if BOOST_COROSIO_HAS_IOCP + auto const invalid = static_cast(~0ull); +#else + auto const invalid = static_cast(-1); +#endif + // Closed: returns the platform sentinel. + BOOST_TEST(f.native_handle() == invalid); + + f.open(tmp.path, file_base::read_only); + BOOST_TEST(f.native_handle() != invalid); + } + + void testOpenReplacesExisting() + { + temp_file tmp1("sf_replace_a_", "first"); + temp_file tmp2("sf_replace_b_", "second"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp1.path, file_base::read_only); + BOOST_TEST(f.is_open()); + + // Reopen on an already-open file closes the previous handle. + f.open(tmp2.path, file_base::read_only); + BOOST_TEST(f.is_open()); + } + +#if BOOST_COROSIO_POSIX + void testOpenSingleThreadedNotSupported() + { + // POSIX file I/O requires the shared thread pool; in single-threaded + // mode the service short-circuits with operation_not_supported. + temp_file tmp("sf_st_", "data"); + io_context ioc(1); + stream_file f(ioc); + + bool caught = false; + try + { + f.open(tmp.path, file_base::read_only); + } + catch (std::system_error const& e) + { + caught = (e.code() == std::errc::operation_not_supported); + } + BOOST_TEST(caught); + } +#endif + + void testReadEmptyBuffer() + { + // Zero-byte read should short-circuit to completion via the + // empty-iovec fast path in read_some. + temp_file tmp("sf_read0_", "hello"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, file_base::read_only); + + bool completed = false; + + auto task = [](stream_file& f_ref, bool& done) -> capy::task<> { + char b; + auto [ec, n] = co_await f_ref.read_some(capy::mutable_buffer(&b, 0)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 0u); + done = true; + }; + capy::run_async(ioc.get_executor())(task(f, completed)); + ioc.run(); + BOOST_TEST(completed); + } + + void testWriteEmptyBuffer() + { + // Same empty-iovec fast path for write_some. + temp_file tmp("sf_write0_"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, + file_base::write_only | file_base::create | file_base::truncate); + + bool completed = false; + + auto task = [](stream_file& f_ref, bool& done) -> capy::task<> { + char const b = 'x'; + auto [ec, n] = co_await f_ref.write_some(capy::const_buffer(&b, 0)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 0u); + done = true; + }; + capy::run_async(ioc.get_executor())(task(f, completed)); + ioc.run(); + BOOST_TEST(completed); + } + // Open flags void testTruncate() @@ -641,6 +779,7 @@ struct stream_file_test testOpenCreateWrite(); testOpenNonexistent(); testOpenExclusive(); + testOpenSyncAllOnWrite(); testSize(); testResize(); @@ -654,6 +793,14 @@ struct stream_file_test testSyncData(); testSyncAll(); testCancelNoOperation(); + testCancelOnClosedFile(); + testNativeHandleClosedAndOpen(); + testOpenReplacesExisting(); + testReadEmptyBuffer(); + testWriteEmptyBuffer(); +#if BOOST_COROSIO_POSIX + testOpenSingleThreadedNotSupported(); +#endif testTruncate(); testAppendMode(); diff --git a/test/unit/tcp_acceptor.cpp b/test/unit/tcp_acceptor.cpp index ff41847df..632e3c95a 100644 --- a/test/unit/tcp_acceptor.cpp +++ b/test/unit/tcp_acceptor.cpp @@ -19,6 +19,12 @@ #include #include +#ifndef _WIN32 +// For the SO_REUSEPORT guard around testReusePort. The corosio public +// option header is platform-agnostic and does not expose this macro. +#include +#endif + #include "context.hpp" #include "test_suite.hpp" @@ -608,6 +614,44 @@ struct tcp_acceptor_test acc.close(); } + void testListenClosedThrows() + { + // listen() on a closed acceptor throws std::logic_error. + io_context ioc(Backend); + tcp_acceptor acc(ioc); + + bool caught = false; + try + { + auto ec = acc.listen(); + (void)ec; + } + catch (std::logic_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + + void testClosedAcceptorAccessors() + { + // cancel() and local_endpoint() on a closed acceptor must + // return without throwing (early return on !is_open()). + io_context ioc(Backend); + tcp_acceptor acc(ioc); + + BOOST_TEST_EQ(acc.is_open(), false); + + acc.cancel(); + BOOST_TEST_EQ(acc.is_open(), false); + + BOOST_TEST(acc.local_endpoint() == endpoint{}); + + // close() on a closed acceptor is a no-op. + acc.close(); + BOOST_TEST_EQ(acc.is_open(), false); + } + void run() { testConstruction(); @@ -643,6 +687,8 @@ struct tcp_acceptor_test testBindClosedAcceptorThrows(); testBindAddressInUse(); testBindError(); + testListenClosedThrows(); + testClosedAcceptorAccessors(); } }; diff --git a/test/unit/tcp_server.cpp b/test/unit/tcp_server.cpp index 98603bc33..3c5d12a6f 100644 --- a/test/unit/tcp_server.cpp +++ b/test/unit/tcp_server.cpp @@ -451,6 +451,384 @@ struct tcp_server_test BOOST_TEST(acc.is_open()); } + void testMoveConstruct() + { + // Verify the noexcept move constructor leaves the source empty + // and the destination with the original state. + io_context ioc; + test_server src(ioc); + auto ec = src.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec); + auto port = src.local_endpoint().port(); + BOOST_TEST(port != 0); + + test_server dst(std::move(src)); + + // Destination retains the bound port; source local_endpoint + // becomes default-constructed (impl moved out). + BOOST_TEST_EQ(dst.local_endpoint().port(), port); + } + + void testMoveAssign() + { + // Move-assign discards the existing impl_ and adopts the source. + io_context ioc; + test_server a(ioc); + test_server b(ioc); + + auto ec = a.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec); + auto port_a = a.local_endpoint().port(); + + ec = b.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec); + + b = std::move(a); + BOOST_TEST_EQ(b.local_endpoint().port(), port_a); + } + + void testLauncherDtorReturnsWorker() + { + // worker_base::run() never invokes the launcher; the launcher + // destructor must return the worker to the idle pool via + // push_sync. After stop() the accept loop completes cleanly. + io_context ioc; + + class no_launch_worker : public tcp_server::worker_base + { + corosio::tcp_socket sock_; + + public: + std::atomic* run_count = nullptr; + + no_launch_worker(io_context& ctx, std::atomic* c) + : sock_(ctx), run_count(c) + { + } + + corosio::tcp_socket& socket() override { return sock_; } + + void run(tcp_server::launcher) override + { + // Drop launcher without invoking it. Its destructor + // must push the worker back to the idle pool. + run_count->fetch_add(1); + sock_.close(); + } + }; + + std::atomic run_count{0}; + + class drop_server : public tcp_server + { + public: + drop_server(io_context& ctx, std::atomic* c) + : tcp_server(ctx, ctx.get_executor()) + { + std::vector> v; + v.push_back(std::make_unique(ctx, c)); + set_workers(std::move(v)); + } + }; + + drop_server srv(ioc, &run_count); + auto ec = srv.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec); + auto port = srv.local_endpoint().port(); + + srv.start(); + + // Two clients exercise the worker pool: the worker is dropped + // back to idle by ~launcher() between connections. + auto driver = [](io_context* ioc, std::uint16_t port, + drop_server* srv) -> capy::task<> { + for (int i = 0; i < 2; ++i) + { + tcp_socket client(*ioc); + client.open(); + auto [cec] = co_await client.connect( + endpoint(ipv4_address::loopback(), port)); + (void)cec; + client.close(); + timer t(*ioc); + t.expires_after(std::chrono::milliseconds(20)); + (void)co_await t.wait(); + } + srv->stop(); + }(&ioc, port, &srv); + + capy::run_async(ioc.get_executor())(std::move(driver)); + ioc.run(); + srv.join(); + + BOOST_TEST(run_count.load() >= 1); + } + + void testMultipleActiveConnections() + { + // Multiple concurrent connections exercise the active list's + // doubly-linked-list bookkeeping (push_back/remove with prev/next). + io_context ioc; + + // Worker that holds the connection open until told to release. + class slow_worker : public tcp_server::worker_base + { + io_context& ctx_; + corosio::tcp_socket sock_; + + public: + explicit slow_worker(io_context& ctx) : ctx_(ctx), sock_(ctx) {} + + corosio::tcp_socket& socket() override { return sock_; } + + void run(tcp_server::launcher launch) override + { + launch( + ctx_.get_executor(), + [](io_context* ctx, + corosio::tcp_socket* s) -> capy::task<> { + // Block on read until the client disconnects. + char buf[64]; + auto [ec, n] = co_await s->read_some( + capy::mutable_buffer(buf, sizeof(buf))); + (void)ec; + (void)n; + s->close(); + (void)ctx; + }(&ctx_, &sock_)); + } + }; + + class multi_server : public tcp_server + { + public: + explicit multi_server(io_context& ctx) + : tcp_server(ctx, ctx.get_executor()) + { + std::vector> v; + v.reserve(4); + for (int i = 0; i < 4; ++i) + v.push_back(std::make_unique(ctx)); + set_workers(std::move(v)); + } + }; + + multi_server srv(ioc); + auto ec = srv.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec); + auto port = srv.local_endpoint().port(); + + srv.start(); + + std::atomic connected{0}; + + // Open 3 simultaneous connections to push three workers into + // the active list, then disconnect them in reverse order so + // both endpoints (head/tail/middle) of active_remove are taken. + auto driver = [](io_context* ioc, std::uint16_t port, + std::atomic* connected, + multi_server* srv) -> capy::task<> { + tcp_socket c1(*ioc), c2(*ioc), c3(*ioc); + c1.open(); c2.open(); c3.open(); + + auto [e1] = co_await c1.connect( + endpoint(ipv4_address::loopback(), port)); + if (!e1) connected->fetch_add(1); + auto [e2] = co_await c2.connect( + endpoint(ipv4_address::loopback(), port)); + if (!e2) connected->fetch_add(1); + auto [e3] = co_await c3.connect( + endpoint(ipv4_address::loopback(), port)); + if (!e3) connected->fetch_add(1); + + // Give the server time to register the connections. + timer t(*ioc); + t.expires_after(std::chrono::milliseconds(50)); + (void)co_await t.wait(); + + // Disconnect middle first, then tail, then head: + // exercises remove from each list position. + c2.close(); + timer t1(*ioc); t1.expires_after(std::chrono::milliseconds(20)); + (void)co_await t1.wait(); + c3.close(); + timer t2(*ioc); t2.expires_after(std::chrono::milliseconds(20)); + (void)co_await t2.wait(); + c1.close(); + timer t3(*ioc); t3.expires_after(std::chrono::milliseconds(20)); + (void)co_await t3.wait(); + + srv->stop(); + }(&ioc, port, &connected, &srv); + + capy::run_async(ioc.get_executor())(std::move(driver)); + ioc.run(); + srv.join(); + + BOOST_TEST(connected.load() >= 1); + } + + void testLauncherDoubleInvokeThrows() + { + // The second invocation of a launcher must throw logic_error. + // We accept the connection but invoke launch twice inside run(). + io_context ioc; + + class throwing_worker : public tcp_server::worker_base + { + io_context& ctx_; + corosio::tcp_socket sock_; + + public: + std::atomic* threw = nullptr; + + throwing_worker(io_context& ctx, std::atomic* t) + : ctx_(ctx), sock_(ctx), threw(t) + { + } + + corosio::tcp_socket& socket() override { return sock_; } + + void run(tcp_server::launcher launch) override + { + launch( + ctx_.get_executor(), + [](corosio::tcp_socket* s) -> capy::task<> { + s->close(); + co_return; + }(&sock_)); + + // Second invocation must throw std::logic_error. + try + { + launch( + ctx_.get_executor(), + [](corosio::tcp_socket*) -> capy::task<> { + co_return; + }(&sock_)); + } + catch (std::logic_error const&) + { + threw->store(true); + } + } + }; + + std::atomic launcher_threw{false}; + + class one_worker_server : public tcp_server + { + public: + one_worker_server(io_context& ctx, std::atomic* t) + : tcp_server(ctx, ctx.get_executor()) + { + std::vector> v; + v.push_back(std::make_unique(ctx, t)); + set_workers(std::move(v)); + } + }; + + one_worker_server srv(ioc, &launcher_threw); + auto ec = srv.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec); + auto port = srv.local_endpoint().port(); + + srv.start(); + + auto client_task = [](io_context* ioc, std::uint16_t port, + one_worker_server* srv) -> capy::task<> { + tcp_socket client(*ioc); + client.open(); + auto [cec] = co_await client.connect( + endpoint(ipv4_address::loopback(), port)); + (void)cec; + client.close(); + + // Give server time to handle the connection, then stop. + timer t(*ioc); + t.expires_after(std::chrono::milliseconds(50)); + (void)co_await t.wait(); + srv->stop(); + }(&ioc, port, &srv); + + capy::run_async(ioc.get_executor())(std::move(client_task)); + ioc.run(); + srv.join(); + + BOOST_TEST(launcher_threw.load()); + } + + void testLocalEndpointOutOfRange() + { + // Index past the bound-ports list returns a default endpoint. + io_context ioc; + test_server srv(ioc); + auto ec = srv.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec); + + BOOST_TEST(srv.local_endpoint(0).port() != 0); + BOOST_TEST(srv.local_endpoint(99) == endpoint{}); + } + + void testWaitersWakeOnWorkerReturn() + { + // With one worker handling two sequential connections, the + // second accept loop iteration must wait for the worker to + // return (exercises pop_awaitable suspend / push_awaitable wake). + io_context ioc; + + class one_worker_server : public tcp_server + { + public: + explicit one_worker_server(io_context& ctx) + : tcp_server(ctx, ctx.get_executor()) + { + set_workers(make_test_workers(ctx, 1)); + } + }; + + one_worker_server srv(ioc); + auto ec = srv.bind(endpoint(ipv4_address::loopback(), 0)); + BOOST_TEST(!ec); + auto port = srv.local_endpoint().port(); + + std::atomic echoed{0}; + + srv.start(); + + auto driver = [](io_context* ioc, std::uint16_t port, + std::atomic* echoed, + one_worker_server* srv) -> capy::task<> { + for (int i = 0; i < 2; ++i) + { + tcp_socket client(*ioc); + client.open(); + auto [cec] = co_await client.connect( + endpoint(ipv4_address::loopback(), port)); + if (cec) + continue; + char msg[] = "ab"; + auto [wec, wn] = + co_await client.write_some(capy::const_buffer(msg, 2)); + if (wec) + continue; + char buf[4]; + auto [rec, rn] = co_await client.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + if (!rec && rn == 2) + echoed->fetch_add(1); + client.close(); + } + srv->stop(); + }(&ioc, port, &echoed, &srv); + + capy::run_async(ioc.get_executor())(std::move(driver)); + ioc.run(); + srv.join(); + + BOOST_TEST_EQ(echoed.load(), 2); + } + void run() { testStopServer(); @@ -465,6 +843,13 @@ struct tcp_server_test testBindErrorNonLocalAcceptor(); testBindErrorNonLocalAddress(); testRelistenAfterClose(); + testMoveConstruct(); + testMoveAssign(); + testLocalEndpointOutOfRange(); + testWaitersWakeOnWorkerReturn(); + testLauncherDoubleInvokeThrows(); + testLauncherDtorReturnsWorker(); + testMultipleActiveConnections(); } }; diff --git a/test/unit/tcp_socket.cpp b/test/unit/tcp_socket.cpp index d98b586ba..28e9a6d09 100644 --- a/test/unit/tcp_socket.cpp +++ b/test/unit/tcp_socket.cpp @@ -100,6 +100,47 @@ struct tcp_socket_test sock.close(); } + void testOpenWhenAlreadyOpen() + { + io_context ioc(Backend); + tcp_socket sock(ioc); + + sock.open(); + BOOST_TEST(sock.is_open()); + + // Second open() on an already-open socket is a no-op. + sock.open(); + BOOST_TEST(sock.is_open()); + } + + void testCancelOnClosedSocket() + { + io_context ioc(Backend); + tcp_socket sock(ioc); + + // cancel() on a closed socket is a no-op (early return). + sock.cancel(); + BOOST_TEST(!sock.is_open()); + } + + void testNativeHandleClosedAndOpen() + { + io_context ioc(Backend); + tcp_socket sock(ioc); + +#if BOOST_COROSIO_HAS_IOCP + auto const invalid = static_cast(~0ull); +#else + auto const invalid = static_cast(-1); +#endif + // Closed: returns the platform sentinel. + BOOST_TEST(sock.native_handle() == invalid); + + sock.open(); + BOOST_TEST(sock.native_handle() != invalid); + sock.close(); + } + void testBindThenConnect() { io_context ioc(Backend); @@ -1593,6 +1634,9 @@ struct tcp_socket_test { testConstruction(); testOpen(); + testOpenWhenAlreadyOpen(); + testCancelOnClosedSocket(); + testNativeHandleClosedAndOpen(); testBind(); testBindThenConnect(); testBindV6(); diff --git a/test/unit/timer.cpp b/test/unit/timer.cpp index 3ae1797fb..32339b4d2 100644 --- a/test/unit/timer.cpp +++ b/test/unit/timer.cpp @@ -362,7 +362,7 @@ struct timer_test }; capy::run_async(ioc.get_executor())(delay_task(delay_timer, t)); - ioc.run_for(std::chrono::milliseconds(500)); + ioc.run(); BOOST_TEST(completed); BOOST_TEST(result_ec == capy::cond::canceled); } @@ -482,6 +482,81 @@ struct timer_test BOOST_TEST(t2_done); } + void testReheapifyOnExpiresAtUpdate() + { + // Push several waiters into the heap, suspend them via run_for so + // their wait actually registers, then move the first timer's + // expiry far into the future — exercises the update_timer + // re-heap (down_heap) branch in timer_service. + io_context ioc(Backend); + timer t1(ioc), t2(ioc), t3(ioc), t4(ioc); + + bool d1 = false, d2 = false, d3 = false, d4 = false; + std::error_code e1, e2, e3, e4; + + t1.expires_after(std::chrono::seconds(5)); + t2.expires_after(std::chrono::milliseconds(20)); + t3.expires_after(std::chrono::milliseconds(30)); + t4.expires_after(std::chrono::milliseconds(40)); + + auto task = [](timer& t, bool& done, std::error_code& ec_out) + -> capy::task<> { + auto [ec] = co_await t.wait(); + done = true; + ec_out = ec; + }; + + capy::run_async(ioc.get_executor())(task(t1, d1, e1)); + capy::run_async(ioc.get_executor())(task(t2, d2, e2)); + capy::run_async(ioc.get_executor())(task(t3, d3, e3)); + capy::run_async(ioc.get_executor())(task(t4, d4, e4)); + + // Let the waiters actually register in the heap, then move + // t1's expiry far past t2..t4 — this cancels t1's pending + // waiter and exercises the down-heap branch. + ioc.poll(); // process any inline work + ioc.restart(); + t1.expires_at(timer::clock_type::now() + std::chrono::seconds(60)); + + ioc.run(); + + BOOST_TEST(d2); + BOOST_TEST(d3); + BOOST_TEST(d4); + BOOST_TEST(!e2); + BOOST_TEST(!e3); + BOOST_TEST(!e4); + // t1's waiter was cancelled by the second expires_at. + BOOST_TEST(d1); + BOOST_TEST(e1 == capy::cond::canceled); + } + + void testTimerFreeListReuseAcrossContexts() + { + // Create timers in one context, destroy the context, then create + // a new context — covers the timer_service shutdown free-list + // delete path and the construct() free-list pop path. + for (int i = 0; i < 3; ++i) + { + io_context ioc(Backend); + timer t1(ioc), t2(ioc), t3(ioc); + t1.expires_after(std::chrono::milliseconds(1)); + t2.expires_after(std::chrono::milliseconds(2)); + t3.expires_after(std::chrono::milliseconds(3)); + + bool done = false; + auto task = [&]() -> capy::task<> { + (void)co_await t1.wait(); + (void)co_await t2.wait(); + (void)co_await t3.wait(); + done = true; + }; + capy::run_async(ioc.get_executor())(task()); + ioc.run(); + BOOST_TEST(done); + } + } + // Multiple waiters on one timer void testMultipleWaiters() @@ -1175,6 +1250,8 @@ struct timer_test // Multiple timer tests testMultipleTimersDifferentExpiry(); testMultipleTimersSameExpiry(); + testReheapifyOnExpiresAtUpdate(); + testTimerFreeListReuseAcrossContexts(); // Multiple waiters on one timer testMultipleWaiters(); diff --git a/test/unit/tls_context.cpp b/test/unit/tls_context.cpp new file mode 100644 index 000000000..46e6de7b0 --- /dev/null +++ b/test/unit/tls_context.cpp @@ -0,0 +1,576 @@ +// +// Copyright (c) 2026 Steve Gerbino +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +// Test that header file is self-contained. +#include + +// Private impl header provides tls_context_data definition for inspection. +#include "src/corosio/src/tls/detail/context_impl.hpp" + +#include "test_utils.hpp" +#include "test_suite.hpp" + +#include +#include +#include +#include +#include +#include + +namespace boost::corosio { + +namespace { + +// RAII helper that creates a temp file with given contents and removes it. +struct temp_file +{ + std::filesystem::path path; + + temp_file(std::string_view prefix, std::string_view contents) + { + path = std::filesystem::temp_directory_path() + / (std::string(prefix) + std::to_string(std::rand())); + std::ofstream ofs(path, std::ios::binary); + ofs.write(contents.data(), static_cast(contents.size())); + } + + ~temp_file() + { + std::error_code ec; + std::filesystem::remove(path, ec); + } + + temp_file(temp_file const&) = delete; + temp_file& operator=(temp_file const&) = delete; + + std::string str() const { return path.string(); } +}; + +// A path guaranteed not to exist. +constexpr char const* nonexistent_path = + "/tmp/corosio_no_such_file_zzz_99999999.pem"; + +} // namespace + +struct tls_context_test +{ + void testDefaultConstruction() + { + tls_context ctx; + auto const& data = detail::get_tls_context_data(ctx); + + BOOST_TEST_EQ(data.entity_certificate, std::string()); + BOOST_TEST(data.entity_cert_format == tls_file_format::pem); + BOOST_TEST(data.min_version == tls_version::tls_1_2); + BOOST_TEST(data.max_version == tls_version::tls_1_3); + BOOST_TEST(data.verification_mode == tls_verify_mode::none); + BOOST_TEST_EQ(data.verify_depth, 100); + BOOST_TEST(!data.use_default_verify_paths); + BOOST_TEST(!data.require_ocsp_staple); + BOOST_TEST(data.revocation == tls_revocation_policy::disabled); + } + + void testCopyAndMove() + { + tls_context a; + BOOST_TEST(!a.use_certificate(test::server_cert_pem, + tls_file_format::pem)); + + // Copy: shares state + tls_context b(a); + auto const& bd = detail::get_tls_context_data(b); + BOOST_TEST_EQ(bd.entity_certificate, std::string(test::server_cert_pem)); + + // Copy-assign + tls_context c; + c = a; + auto const& cd = detail::get_tls_context_data(c); + BOOST_TEST_EQ(cd.entity_certificate, std::string(test::server_cert_pem)); + + // Move-construct + tls_context d(std::move(b)); + auto const& dd = detail::get_tls_context_data(d); + BOOST_TEST_EQ(dd.entity_certificate, std::string(test::server_cert_pem)); + + // Move-assign + tls_context e; + e = std::move(c); + auto const& ed = detail::get_tls_context_data(e); + BOOST_TEST_EQ(ed.entity_certificate, std::string(test::server_cert_pem)); + } + + // + // Credential loading (in-memory) + // + + void testUseCertificateInMemory() + { + tls_context ctx; + auto ec = ctx.use_certificate( + test::server_cert_pem, tls_file_format::pem); + BOOST_TEST(!ec); + + auto const& data = detail::get_tls_context_data(ctx); + BOOST_TEST_EQ(data.entity_certificate, + std::string(test::server_cert_pem)); + BOOST_TEST(data.entity_cert_format == tls_file_format::pem); + + // DER format selector should also be stored without parsing + tls_context der_ctx; + ec = der_ctx.use_certificate("\x30\x82\x00\x00", tls_file_format::der); + BOOST_TEST(!ec); + BOOST_TEST(detail::get_tls_context_data(der_ctx).entity_cert_format + == tls_file_format::der); + } + + void testUseCertificateChainInMemory() + { + tls_context ctx; + auto ec = ctx.use_certificate_chain(test::server_fullchain_pem); + BOOST_TEST(!ec); + + auto const& data = detail::get_tls_context_data(ctx); + BOOST_TEST_EQ(data.certificate_chain, + std::string(test::server_fullchain_pem)); + } + + void testUsePrivateKeyInMemory() + { + tls_context ctx; + auto ec = ctx.use_private_key( + test::server_key_pem, tls_file_format::pem); + BOOST_TEST(!ec); + + auto const& data = detail::get_tls_context_data(ctx); + BOOST_TEST_EQ(data.private_key, std::string(test::server_key_pem)); + BOOST_TEST(data.private_key_format == tls_file_format::pem); + + // Garbage PEM is accepted at storage time (no parsing in tls_context) + tls_context bogus; + ec = bogus.use_private_key( + "-----BEGIN PRIVATE KEY-----\nnot-base64!@#\n", + tls_file_format::pem); + BOOST_TEST(!ec); + } + + // + // Credential loading (from file) + // + + void testUseCertificateFile() + { + // Success: read existing temp file + { + temp_file f("corosio_cert_", test::server_cert_pem); + tls_context ctx; + auto ec = ctx.use_certificate_file(f.str(), tls_file_format::pem); + BOOST_TEST(!ec); + BOOST_TEST_EQ(detail::get_tls_context_data(ctx).entity_certificate, + std::string(test::server_cert_pem)); + } + + // Failure: nonexistent file -> ENOENT + { + tls_context ctx; + auto ec = ctx.use_certificate_file( + nonexistent_path, tls_file_format::pem); + BOOST_TEST(ec); + BOOST_TEST_EQ(ec.value(), ENOENT); + } + } + + void testUseCertificateChainFile() + { + // Success + { + temp_file f("corosio_chain_", test::server_fullchain_pem); + tls_context ctx; + auto ec = ctx.use_certificate_chain_file(f.str()); + BOOST_TEST(!ec); + BOOST_TEST_EQ(detail::get_tls_context_data(ctx).certificate_chain, + std::string(test::server_fullchain_pem)); + } + + // Missing file + { + tls_context ctx; + auto ec = ctx.use_certificate_chain_file(nonexistent_path); + BOOST_TEST(ec); + BOOST_TEST_EQ(ec.value(), ENOENT); + } + } + + void testUsePrivateKeyFile() + { + // Success + { + temp_file f("corosio_key_", test::server_key_pem); + tls_context ctx; + auto ec = ctx.use_private_key_file(f.str(), tls_file_format::pem); + BOOST_TEST(!ec); + BOOST_TEST_EQ(detail::get_tls_context_data(ctx).private_key, + std::string(test::server_key_pem)); + } + + // Missing file + { + tls_context ctx; + auto ec = ctx.use_private_key_file( + nonexistent_path, tls_file_format::pem); + BOOST_TEST(ec); + BOOST_TEST_EQ(ec.value(), ENOENT); + } + } + + // + // PKCS#12 (currently unsupported) + // + + void testPkcs12Unsupported() + { + tls_context ctx; + auto ec = ctx.use_pkcs12("not-pkcs12-data", "password"); + BOOST_TEST(ec); + BOOST_TEST(ec == std::make_error_code(std::errc::function_not_supported)); + + ec = ctx.use_pkcs12_file("/some/path", "password"); + BOOST_TEST(ec); + BOOST_TEST(ec == std::make_error_code(std::errc::function_not_supported)); + } + + // + // Trust anchors + // + + void testAddCertificateAuthority() + { + tls_context ctx; + auto ec = ctx.add_certificate_authority(test::ca_cert_pem); + BOOST_TEST(!ec); + ec = ctx.add_certificate_authority(test::root_ca_cert_pem); + BOOST_TEST(!ec); + + auto const& data = detail::get_tls_context_data(ctx); + BOOST_TEST_EQ(data.ca_certificates.size(), 2u); + BOOST_TEST_EQ(data.ca_certificates[0], std::string(test::ca_cert_pem)); + BOOST_TEST_EQ(data.ca_certificates[1], + std::string(test::root_ca_cert_pem)); + } + + void testLoadVerifyFile() + { + // Success + { + temp_file f("corosio_ca_", test::root_ca_cert_pem); + tls_context ctx; + auto ec = ctx.load_verify_file(f.str()); + BOOST_TEST(!ec); + auto const& data = detail::get_tls_context_data(ctx); + BOOST_TEST_EQ(data.ca_certificates.size(), 1u); + BOOST_TEST_EQ(data.ca_certificates[0], + std::string(test::root_ca_cert_pem)); + } + + // Missing file + { + tls_context ctx; + auto ec = ctx.load_verify_file(nonexistent_path); + BOOST_TEST(ec); + BOOST_TEST_EQ(ec.value(), ENOENT); + } + } + + void testVerifyPaths() + { + tls_context ctx; + auto ec = ctx.add_verify_path("/etc/ssl/certs"); + BOOST_TEST(!ec); + ec = ctx.add_verify_path("/usr/local/etc/ssl"); + BOOST_TEST(!ec); + + auto const& data = detail::get_tls_context_data(ctx); + BOOST_TEST_EQ(data.verify_paths.size(), 2u); + BOOST_TEST_EQ(data.verify_paths[0], std::string("/etc/ssl/certs")); + BOOST_TEST_EQ(data.verify_paths[1], std::string("/usr/local/etc/ssl")); + } + + void testDefaultVerifyPaths() + { + tls_context ctx; + BOOST_TEST(!detail::get_tls_context_data(ctx).use_default_verify_paths); + + auto ec = ctx.set_default_verify_paths(); + BOOST_TEST(!ec); + BOOST_TEST(detail::get_tls_context_data(ctx).use_default_verify_paths); + } + + // + // Protocol configuration + // + + void testProtocolVersionRoundTrip() + { + tls_context ctx; + + auto ec = ctx.set_min_protocol_version(tls_version::tls_1_3); + BOOST_TEST(!ec); + BOOST_TEST(detail::get_tls_context_data(ctx).min_version + == tls_version::tls_1_3); + + ec = ctx.set_max_protocol_version(tls_version::tls_1_2); + BOOST_TEST(!ec); + BOOST_TEST(detail::get_tls_context_data(ctx).max_version + == tls_version::tls_1_2); + + // Reset min/max + ec = ctx.set_min_protocol_version(tls_version::tls_1_2); + BOOST_TEST(!ec); + ec = ctx.set_max_protocol_version(tls_version::tls_1_3); + BOOST_TEST(!ec); + } + + void testCiphersuites() + { + tls_context ctx; + auto ec = ctx.set_ciphersuites("ECDHE+AESGCM"); + BOOST_TEST(!ec); + BOOST_TEST_EQ(detail::get_tls_context_data(ctx).ciphersuites, + std::string("ECDHE+AESGCM")); + + // Empty string is accepted at storage time + ec = ctx.set_ciphersuites(""); + BOOST_TEST(!ec); + BOOST_TEST_EQ(detail::get_tls_context_data(ctx).ciphersuites, + std::string()); + } + + void testAlpn() + { + tls_context ctx; + + // Initially empty + BOOST_TEST_EQ(detail::get_tls_context_data(ctx).alpn_protocols.size(), + 0u); + + // Set list + auto ec = ctx.set_alpn({"h2", "http/1.1"}); + BOOST_TEST(!ec); + auto const& data = detail::get_tls_context_data(ctx); + BOOST_TEST_EQ(data.alpn_protocols.size(), 2u); + BOOST_TEST_EQ(data.alpn_protocols[0], std::string("h2")); + BOOST_TEST_EQ(data.alpn_protocols[1], std::string("http/1.1")); + + // Reset replaces the list + ec = ctx.set_alpn({"http/1.1"}); + BOOST_TEST(!ec); + BOOST_TEST_EQ(data.alpn_protocols.size(), 1u); + BOOST_TEST_EQ(data.alpn_protocols[0], std::string("http/1.1")); + + // Clear by passing empty list + ec = ctx.set_alpn({}); + BOOST_TEST(!ec); + BOOST_TEST_EQ(data.alpn_protocols.size(), 0u); + } + + // + // Verification + // + + void testVerifyModeAndDepth() + { + tls_context ctx; + + auto ec = ctx.set_verify_mode(tls_verify_mode::peer); + BOOST_TEST(!ec); + BOOST_TEST(detail::get_tls_context_data(ctx).verification_mode + == tls_verify_mode::peer); + + ec = ctx.set_verify_mode(tls_verify_mode::require_peer); + BOOST_TEST(!ec); + BOOST_TEST(detail::get_tls_context_data(ctx).verification_mode + == tls_verify_mode::require_peer); + + ec = ctx.set_verify_depth(5); + BOOST_TEST(!ec); + BOOST_TEST_EQ(detail::get_tls_context_data(ctx).verify_depth, 5); + } + + void testHostname() + { + tls_context ctx; + BOOST_TEST_EQ(detail::get_tls_context_data(ctx).hostname, std::string()); + + ctx.set_hostname("api.example.com"); + BOOST_TEST_EQ(detail::get_tls_context_data(ctx).hostname, + std::string("api.example.com")); + + // Overwrite + ctx.set_hostname("other.example.com"); + BOOST_TEST_EQ(detail::get_tls_context_data(ctx).hostname, + std::string("other.example.com")); + } + + void testServernameCallback() + { + tls_context ctx; + + bool invoked = false; + std::string saw; + ctx.set_servername_callback( + [&](std::string_view name) -> bool { + invoked = true; + saw = std::string(name); + return name == "ok.example.com"; + }); + + auto const& cb = detail::get_tls_context_data(ctx).servername_callback; + BOOST_TEST(static_cast(cb)); + + // Invoke through stored std::function + BOOST_TEST(cb("ok.example.com")); + BOOST_TEST(invoked); + BOOST_TEST_EQ(saw, std::string("ok.example.com")); + + BOOST_TEST(!cb("nope.example.com")); + } + + void testPasswordCallback() + { + tls_context ctx; + + bool invoked = false; + ctx.set_password_callback( + [&](std::size_t max_len, tls_password_purpose purpose) + -> std::string { + invoked = true; + BOOST_TEST(max_len > 0); + BOOST_TEST(purpose == tls_password_purpose::for_reading + || purpose == tls_password_purpose::for_writing); + return std::string("secret"); + }); + + auto const& cb = detail::get_tls_context_data(ctx).password_callback; + BOOST_TEST(static_cast(cb)); + auto pw = cb(64, tls_password_purpose::for_reading); + BOOST_TEST_EQ(pw, std::string("secret")); + BOOST_TEST(invoked); + } + + // + // Revocation + // + + void testAddCrl() + { + tls_context ctx; + auto ec = ctx.add_crl("-----BEGIN X509 CRL-----\nABC\n-----END X509 CRL-----\n"); + BOOST_TEST(!ec); + ec = ctx.add_crl("second-crl-data"); + BOOST_TEST(!ec); + + auto const& data = detail::get_tls_context_data(ctx); + BOOST_TEST_EQ(data.crls.size(), 2u); + } + + void testAddCrlFile() + { + // Success + { + temp_file f("corosio_crl_", "fake-crl-bytes"); + tls_context ctx; + auto ec = ctx.add_crl_file(f.str()); + BOOST_TEST(!ec); + auto const& data = detail::get_tls_context_data(ctx); + BOOST_TEST_EQ(data.crls.size(), 1u); + BOOST_TEST_EQ(data.crls[0], std::string("fake-crl-bytes")); + } + + // Missing file + { + tls_context ctx; + auto ec = ctx.add_crl_file(nonexistent_path); + BOOST_TEST(ec); + BOOST_TEST_EQ(ec.value(), ENOENT); + } + } + + void testOcspStaple() + { + tls_context ctx; + auto ec = ctx.set_ocsp_staple("\x30\x82\x01\x00 binary ocsp blob"); + BOOST_TEST(!ec); + BOOST_TEST(!detail::get_tls_context_data(ctx).ocsp_staple.empty()); + } + + void testRequireOcspStaple() + { + tls_context ctx; + BOOST_TEST(!detail::get_tls_context_data(ctx).require_ocsp_staple); + + ctx.set_require_ocsp_staple(true); + BOOST_TEST(detail::get_tls_context_data(ctx).require_ocsp_staple); + + ctx.set_require_ocsp_staple(false); + BOOST_TEST(!detail::get_tls_context_data(ctx).require_ocsp_staple); + } + + void testRevocationPolicy() + { + tls_context ctx; + ctx.set_revocation_policy(tls_revocation_policy::soft_fail); + BOOST_TEST(detail::get_tls_context_data(ctx).revocation + == tls_revocation_policy::soft_fail); + + ctx.set_revocation_policy(tls_revocation_policy::hard_fail); + BOOST_TEST(detail::get_tls_context_data(ctx).revocation + == tls_revocation_policy::hard_fail); + + ctx.set_revocation_policy(tls_revocation_policy::disabled); + BOOST_TEST(detail::get_tls_context_data(ctx).revocation + == tls_revocation_policy::disabled); + } + + void run() + { + testDefaultConstruction(); + testCopyAndMove(); + + testUseCertificateInMemory(); + testUseCertificateChainInMemory(); + testUsePrivateKeyInMemory(); + testUseCertificateFile(); + testUseCertificateChainFile(); + testUsePrivateKeyFile(); + testPkcs12Unsupported(); + + testAddCertificateAuthority(); + testLoadVerifyFile(); + testVerifyPaths(); + testDefaultVerifyPaths(); + + testProtocolVersionRoundTrip(); + testCiphersuites(); + testAlpn(); + + testVerifyModeAndDepth(); + testHostname(); + testServernameCallback(); + testPasswordCallback(); + + testAddCrl(); + testAddCrlFile(); + testOcspStaple(); + testRequireOcspStaple(); + testRevocationPolicy(); + } +}; + +TEST_SUITE(tls_context_test, "boost.corosio.tls_context"); + +} // namespace boost::corosio diff --git a/test/unit/udp_socket.cpp b/test/unit/udp_socket.cpp index 7d550d926..537a0541c 100644 --- a/test/unit/udp_socket.cpp +++ b/test/unit/udp_socket.cpp @@ -144,6 +144,115 @@ struct udp_socket_test BOOST_TEST(caught); } + void testSetOptionClosedThrows() + { + io_context ioc(Backend); + udp_socket sock(ioc); + + bool caught = false; + try + { + sock.set_option(socket_option::broadcast(true)); + } + catch (std::logic_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + + void testGetOptionClosedThrows() + { + io_context ioc(Backend); + udp_socket sock(ioc); + + bool caught = false; + try + { + (void)sock.get_option(); + } + catch (std::logic_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + + void testSendToClosedThrows() + { + io_context ioc(Backend); + udp_socket sock(ioc); + + char const msg[] = "x"; + bool caught = false; + try + { + (void)sock.send_to( + capy::const_buffer(msg, sizeof(msg)), + endpoint(ipv4_address::loopback(), 1)); + } + catch (std::logic_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + + void testRecvFromClosedThrows() + { + io_context ioc(Backend); + udp_socket sock(ioc); + + char buf[16]; + endpoint src; + bool caught = false; + try + { + (void)sock.recv_from(capy::mutable_buffer(buf, sizeof(buf)), src); + } + catch (std::logic_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + + void testSendClosedThrows() + { + io_context ioc(Backend); + udp_socket sock(ioc); + + char const msg[] = "x"; + bool caught = false; + try + { + (void)sock.send(capy::const_buffer(msg, sizeof(msg))); + } + catch (std::logic_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + + void testRecvClosedThrows() + { + io_context ioc(Backend); + udp_socket sock(ioc); + + char buf[16]; + bool caught = false; + try + { + (void)sock.recv(capy::mutable_buffer(buf, sizeof(buf))); + } + catch (std::logic_error const&) + { + caught = true; + } + BOOST_TEST(caught); + } + void testBindAddressInUse() { io_context ioc(Backend); @@ -163,6 +272,50 @@ struct udp_socket_test sock2.close(); } + void testClosedAccessorsReturnDefaults() + { + // Accessors on a closed socket must not throw and must + // return sentinel/default values. + io_context ioc(Backend); + udp_socket sock(ioc); + + BOOST_TEST_EQ(sock.is_open(), false); +#if BOOST_COROSIO_HAS_IOCP + auto const invalid = static_cast(~0ull); +#else + auto const invalid = static_cast(-1); +#endif + BOOST_TEST_EQ(sock.native_handle(), invalid); + BOOST_TEST(sock.local_endpoint() == endpoint{}); + BOOST_TEST(sock.remote_endpoint() == endpoint{}); + + // cancel() on a closed socket is a no-op (early return). + sock.cancel(); + BOOST_TEST_EQ(sock.is_open(), false); + + // close() on a closed socket is a no-op (early return). + sock.close(); + BOOST_TEST_EQ(sock.is_open(), false); + } + + void testOpenIdempotent() + { + // Re-opening an already-open socket returns without re-creating + // the underlying handle (early return at the head of open()). + io_context ioc(Backend); + udp_socket sock(ioc); + + sock.open(); + BOOST_TEST(sock.is_open()); + auto nh = sock.native_handle(); + + sock.open(); + BOOST_TEST(sock.is_open()); + BOOST_TEST_EQ(sock.native_handle(), nh); + + sock.close(); + } + void testBindNonLocalAddress() { io_context ioc(Backend); @@ -870,7 +1023,8 @@ struct udp_socket_test BOOST_TEST_EQ(ec, std::error_code{}); auto recv_ep = receiver.local_endpoint(); - // Join may fail in CI environments without multicast routing + // Join may fail with an environment-specific error in CI + // without multicast routing; skip the rest of the test. try { receiver.set_option( @@ -912,6 +1066,160 @@ struct udp_socket_test sender.close(); } + // Multicast set_option tests below accept any std::system_error. + // Multicast routing is environment-specific: we've observed at + // least EADDRNOTAVAIL (Linux containers), EINVAL (BSD strict + // RFC 3493 reading), and ENODEV (interface-scoped without scope id) + // across CI runs. The library code path is exercised either way; + // these tests are for coverage of set_option, not for asserting + // that the kernel completes the request. + + void testMulticastLeaveV4() + { + io_context ioc(Backend); + udp_socket sock(ioc); + sock.open(); + + try + { + sock.set_option( + socket_option::join_group_v4(ipv4_address("239.255.0.2"))); + sock.set_option( + socket_option::leave_group_v4(ipv4_address("239.255.0.2"))); + } + catch (std::system_error const&) + { + BOOST_TEST_PASS(); + } + + sock.close(); + } + + void testMulticastJoinLeaveV6() + { + io_context ioc(Backend); + udp_socket sock(ioc); + sock.open(udp::v6()); + + try + { + sock.set_option( + socket_option::join_group_v6(ipv6_address("ff02::1"))); + sock.set_option( + socket_option::leave_group_v6(ipv6_address("ff02::1"))); + } + catch (std::system_error const&) + { + BOOST_TEST_PASS(); + } + + sock.close(); + } + + void testMulticastInterfaceV4() + { + io_context ioc(Backend); + udp_socket sock(ioc); + sock.open(); + + try + { + sock.set_option( + socket_option::multicast_interface_v4(ipv4_address::any())); + } + catch (std::system_error const&) + { + BOOST_TEST_PASS(); + } + + sock.close(); + } + + void testMulticastInterfaceV6() + { + io_context ioc(Backend); + udp_socket sock(ioc); + sock.open(udp::v6()); + + try + { + sock.set_option(socket_option::multicast_interface_v6(0)); + auto opt = sock.get_option(); + BOOST_TEST_EQ(opt.value(), 0); + } + catch (std::system_error const&) + { + BOOST_TEST_PASS(); + } + + sock.close(); + } + + void testBufferSizeBoundary() + { + io_context ioc(Backend); + udp_socket sock(ioc); + sock.open(); + + // Linux clamps SO_RCVBUF=0 to a minimum and reports success; + // BSD platforms (macOS, FreeBSD) reject 0 with EINVAL. + try + { + sock.set_option(socket_option::receive_buffer_size(0)); + int rcv = + sock.get_option().value(); + // Linux clamps to a minimum > 0; Windows allows 0. + BOOST_TEST(rcv >= 0); + } + catch (std::system_error const&) + { + BOOST_TEST_PASS(); + } + + try + { + sock.set_option(socket_option::send_buffer_size(0)); + int snd = + sock.get_option().value(); + // Linux clamps to a minimum > 0; Windows allows 0. + BOOST_TEST(snd >= 0); + } + catch (std::system_error const&) + { + BOOST_TEST_PASS(); + } + + // Large value: set succeeds on every platform, but the kernel + // may clamp to net.core.rmem_max (often a few hundred KiB in + // constrained containers). Only assert non-zero. + sock.set_option(socket_option::receive_buffer_size(1024 * 1024)); + int rcv = sock.get_option().value(); + BOOST_TEST(rcv > 0); + + sock.close(); + } + + void testWrongProtocolNoDelayOnUdp() + { + // TCP_NODELAY is meaningful only on TCP; setting on UDP must error. + io_context ioc(Backend); + udp_socket sock(ioc); + sock.open(); + + bool caught = false; + try + { + sock.set_option(socket_option::no_delay(true)); + } + catch (std::system_error const&) + { + caught = true; + } + BOOST_TEST(caught); + + sock.close(); + } + void run() { testConstruction(); @@ -922,8 +1230,16 @@ struct udp_socket_test testBind(); testBindV6(); testBindClosedSocketThrows(); + testSetOptionClosedThrows(); + testGetOptionClosedThrows(); + testSendToClosedThrows(); + testRecvFromClosedThrows(); + testSendClosedThrows(); + testRecvClosedThrows(); testBindAddressInUse(); testBindNonLocalAddress(); + testClosedAccessorsReturnDefaults(); + testOpenIdempotent(); testSetOption(); testSendRecvLoopback(); testSendRecvV6Loopback(); @@ -943,6 +1259,12 @@ struct udp_socket_test testMulticastLoopHops(); testMulticastLoopHopsV6(); testMulticastJoinV4(); + testMulticastLeaveV4(); + testMulticastJoinLeaveV6(); + testMulticastInterfaceV4(); + testMulticastInterfaceV6(); + testBufferSizeBoundary(); + testWrongProtocolNoDelayOnUdp(); } }; diff --git a/test/unit/wait.cpp b/test/unit/wait.cpp index e57b1a712..37fe38fa0 100644 --- a/test/unit/wait.cpp +++ b/test/unit/wait.cpp @@ -10,8 +10,9 @@ // Test that header is self-contained. #include -#include - +#include +#include +#include #include #include #include @@ -20,6 +21,7 @@ #include #include +#include #include #include @@ -32,47 +34,11 @@ #include #include -#if BOOST_COROSIO_POSIX -#include -#include -#include - -#include -#include - -#include -#endif - #include "context.hpp" #include "test_suite.hpp" namespace boost::corosio { -#if BOOST_COROSIO_POSIX -namespace { - -std::string -make_temp_socket_path() -{ - char tmpl[] = "/tmp/corosio_wait_XXXXXX"; - if (!::mkdtemp(tmpl)) - throw std::runtime_error("mkdtemp failed"); - std::string path(tmpl); - path += "/sock"; - return path; -} - -void -cleanup_path(std::string const& path) -{ - ::unlink(path.c_str()); - auto dir = path.substr(0, path.rfind('/')); - ::rmdir(dir.c_str()); -} - -} // namespace -#endif - template struct wait_test { @@ -144,13 +110,13 @@ struct wait_test BOOST_TEST(!wait_ec); } -#if BOOST_COROSIO_POSIX // local_stream_socket wait_read fires when the peer writes. void testWaitOnLocalStream() { io_context ioc(Backend); auto ex = ioc.get_executor(); - auto path = make_temp_socket_path(); + test::temp_socket_dir tmp; + auto path = tmp.path(); local_stream_acceptor acc(ioc); acc.open(); @@ -196,12 +162,9 @@ struct wait_test capy::run_async(ex)(writer()); ioc.run(); - cleanup_path(path); - BOOST_TEST(wait_done); BOOST_TEST(!wait_ec); } -#endif // BOOST_COROSIO_POSIX // Cancellation via socket.cancel() yields operation_canceled. void testCancellation() @@ -360,9 +323,7 @@ struct wait_test { testWaitReadAndNoConsume(); testWaitWriteImmediate(); -#if BOOST_COROSIO_POSIX testWaitOnLocalStream(); -#endif testCancellation(); testAcceptorWait(); testWaitOnUdp(); diff --git a/test/unit/wolfssl_stream.cpp b/test/unit/wolfssl_stream.cpp index 7b2d338e1..72f6e041c 100644 --- a/test/unit/wolfssl_stream.cpp +++ b/test/unit/wolfssl_stream.cpp @@ -56,6 +56,26 @@ struct wolfssl_stream_test BOOST_TEST(stream.name() == "wolfssl"); } + /** Exercise next_layer() accessors (const and non-const). */ + void testNextLayer() + { + using namespace test; + + io_context ioc; + auto ctx = make_client_context(); + tcp_socket sock(ioc); + wolfssl_stream stream(&sock, ctx); + + capy::any_stream& mutable_next = stream.next_layer(); + (void)mutable_next; + + wolfssl_stream const& cref = stream; + capy::any_stream const& const_next = cref.next_layer(); + (void)const_next; + + BOOST_TEST(&mutable_next == &const_next); + } + /** Test certificate chain validation (WolfSSL-specific). WolfSSL has limited certificate chain support compared to OpenSSL. @@ -112,6 +132,7 @@ struct wolfssl_stream_test testCertificateChain(); testName(); + testNextLayer(); } };