From 3e44705383f8971a9862d68f2bd5e5774cbe2306 Mon Sep 17 00:00:00 2001 From: Aditya Rao <82911340+adityarao2005@users.noreply.github.com> Date: Mon, 16 Feb 2026 00:52:28 -0500 Subject: [PATCH] Revert "Add UDP multicast support" --- .github/workflows/build.yml | 1 - CMakeLists.txt | 6 +- include/webcraft/async/io/socket.hpp | 142 -------------- src/webcraft/async_udp.cpp | 266 +++----------------------- tests/CMakeLists.txt | 7 - tests/src/test_async_io_multicast.cpp | 189 ------------------ 6 files changed, 27 insertions(+), 584 deletions(-) delete mode 100644 tests/src/test_async_io_multicast.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4bc1eca..c3d5957 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -107,7 +107,6 @@ jobs: -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} -S ${{ github.workspace }} -DWEBCRAFT_BUILD_TESTS=ON - -DWEBCRAFT_HAS_MULTICAST=OFF - name: Build not for MacOS if: ${{ matrix.os != 'macos-latest' }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a90e0e..619e8f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -87,11 +87,7 @@ install( DESTINATION share/WebCraft ) -# --- 4. Multicast support --- -# Set to OFF on environments where multicast loopback is unreliable (e.g. some macOS CI runners). -option(WEBCRAFT_HAS_MULTICAST "Build and run tests that require multicast/loopback support" ON) - -# --- 5. Testing --- +# --- 4. Testing --- option(WEBCRAFT_BUILD_TESTS "Build WebCraft tests" OFF) if(WEBCRAFT_BUILD_TESTS) enable_testing() diff --git a/include/webcraft/async/io/socket.hpp b/include/webcraft/async/io/socket.hpp index ff3c7fc..d49333f 100644 --- a/include/webcraft/async/io/socket.hpp +++ b/include/webcraft/async/io/socket.hpp @@ -7,10 +7,6 @@ #include "core.hpp" #include -#include -#include -#include -#include namespace webcraft::async::io::socket { @@ -20,31 +16,6 @@ namespace webcraft::async::io::socket uint16_t port; }; - /// Placeholder for options when joining a multicast group. Currently empty: an instance - /// of this type indicates default multicast join behavior. Fields may be added in the future. - struct multicast_join_options - { - }; - - /// Represents a multicast group address. Use resolve() to create from a string (e.g. "239.255.0.1"). - struct multicast_group - { - std::string host; ///< Multicast group address (e.g. "239.255.0.1") - uint16_t port{0}; ///< Port used when sending to the group; must be set to the desired (non-zero) UDP port before calling send functions. - - /// Resolve a multicast group from an address string (IPv4 or IPv6 multicast address). - /// \throws std::invalid_argument if addr is not a valid multicast address. - static multicast_group resolve(std::string_view addr) - { - std::string s(addr); - if (!detail::is_multicast_address(s)) - throw std::invalid_argument("Not a multicast address: " + s); - multicast_group g; - g.host = std::move(s); - return g; - } - }; - enum class ip_version { IPv4, @@ -59,52 +30,6 @@ namespace webcraft::async::io::socket namespace detail { - /// Returns true if the given address string is a valid IPv4 or IPv6 multicast address. - inline bool is_multicast_address(const std::string &addr) - { - if (addr.empty()) return false; - if (addr.find('.') != std::string::npos) - { - // IPv4: 224.0.0.0 - 239.255.255.255 - unsigned a = 0, b = 0, c = 0, d = 0; - int n = 0; - if (std::sscanf(addr.c_str(), "%u.%u.%u.%u%n", &a, &b, &c, &d, &n) != 4) return false; - if (addr.size() != static_cast(n)) return false; - if (a > 255u || b > 255u || c > 255u || d > 255u) return false; - if (a < 224u || a > 239u) return false; - return true; - } - if (addr.find(':') != std::string::npos) - { - // IPv6: ff00::/8 — first 16-bit segment must be 0xff00–0xffff - std::size_t i = 0; - while (i < addr.size() && addr[i] == ':') ++i; - while (i < addr.size()) - { - std::size_t start = i; - while (i < addr.size() && addr[i] != ':') - { - char ch = addr[i]; - if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')) - ++i; - else - return false; - } - if (i > start) - { - if (i - start >= 2u && i - start <= 4u) - { - if ((addr[start] == 'f' || addr[start] == 'F') && (addr[start + 1] == 'f' || addr[start + 1] == 'F')) - return true; - } - return false; - } - if (i < addr.size()) ++i; - } - return false; - } - return false; - } class tcp_descriptor_base { @@ -152,11 +77,6 @@ namespace webcraft::async::io::socket virtual task recvfrom(std::span buffer, connection_info &info) = 0; virtual task sendto(std::span buffer, const connection_info &info) = 0; - - /// Join a multicast group. Optional; no-op if not supported (e.g. mock). - virtual void join_group(const multicast_group &group, const multicast_join_options &opts) { (void)group; (void)opts; } - /// Leave a multicast group. - virtual void leave_group(const multicast_group &group) { (void)group; } }; std::shared_ptr make_tcp_socket_descriptor(); @@ -432,63 +352,6 @@ namespace webcraft::async::io::socket } }; - /// UDP socket that can join multicast groups and send/receive to/from those groups. - class multicast_socket - { - private: - std::shared_ptr descriptor; - - public: - explicit multicast_socket(std::shared_ptr desc) : descriptor(std::move(desc)) {} - ~multicast_socket() - { - fire_and_forget(close()); - } - - void bind(const connection_info &info) - { - descriptor->bind(info); - } - - /// Join a multicast group. Call after bind(). - void join(const multicast_group &group, const multicast_join_options &opts = {}) - { - descriptor->join_group(group, opts); - } - - /// Leave a multicast group. - void leave(const multicast_group &group) - { - descriptor->leave_group(group); - } - - task recvfrom(std::span buffer, connection_info &info) - { - return descriptor->recvfrom(buffer, info); - } - - /// Send data to a multicast group. group.port must be non-zero. - /// \throws std::invalid_argument if group.port is 0. - task sendto(std::span buffer, const multicast_group &group) - { - if (group.port == 0) - throw std::invalid_argument("multicast_group::port must be set (non-zero) before sendto"); - connection_info info; - info.host = group.host; - info.port = group.port; - return descriptor->sendto(buffer, info); - } - - task close() - { - if (descriptor) - { - co_await descriptor->close(); - descriptor.reset(); - } - } - }; - inline tcp_socket make_tcp_socket() { return tcp_socket(detail::make_tcp_socket_descriptor()); @@ -503,9 +366,4 @@ namespace webcraft::async::io::socket { return udp_socket(detail::make_udp_socket_descriptor(version)); } - - inline multicast_socket make_multicast_socket(std::optional version = std::nullopt) - { - return multicast_socket(detail::make_udp_socket_descriptor(version)); - } } \ No newline at end of file diff --git a/src/webcraft/async_udp.cpp b/src/webcraft/async_udp.cpp index 78f2d40..c467921 100644 --- a/src/webcraft/async_udp.cpp +++ b/src/webcraft/async_udp.cpp @@ -23,7 +23,6 @@ using namespace webcraft::async::io::socket::detail; #include #include #include -#include #elif defined(_WIN32) @@ -38,7 +37,6 @@ using namespace webcraft::async::io::socket::detail; #include #include #include -#include #endif @@ -564,82 +562,6 @@ class iocp_udp_socket_descriptor : public webcraft::async::io::socket::detail::u co_return bytes_sent; } - - void join_group(const webcraft::async::io::socket::multicast_group &group, const webcraft::async::io::socket::multicast_join_options &) override - { - if (socket == INVALID_SOCKET) return; - struct addrinfo hints{}; - hints.ai_family = AF_UNSPEC; - hints.ai_flags = AI_NUMERICHOST; - struct addrinfo *res = nullptr; - if (getaddrinfo(group.host.c_str(), nullptr, &hints, &res) != 0 || !res) - throw std::invalid_argument("Invalid multicast address: " + group.host); - if (res->ai_family == AF_INET) - { - auto *sa = (struct sockaddr_in *)res->ai_addr; - if (!IN_MULTICAST(ntohl(sa->sin_addr.s_addr))) - { - freeaddrinfo(res); - throw std::invalid_argument("Not a multicast address: " + group.host); - } - ip_mreq mreq{}; - mreq.imr_multiaddr = sa->sin_addr; - mreq.imr_interface.s_addr = INADDR_ANY; - freeaddrinfo(res); - if (setsockopt(socket, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char *)&mreq, sizeof(mreq)) == SOCKET_ERROR) - throw webcraft::async::detail::windows::overlapped_winsock2_runtime_error("Failed to join IPv4 multicast group"); - return; - } - if (res->ai_family == AF_INET6) - { - auto *sa = (struct sockaddr_in6 *)res->ai_addr; - if (!IN6_IS_ADDR_MULTICAST(&sa->sin6_addr)) - { - freeaddrinfo(res); - throw std::invalid_argument("Not a multicast address: " + group.host); - } - ipv6_mreq mreq6{}; - mreq6.ipv6mr_multiaddr = sa->sin6_addr; - mreq6.ipv6mr_interface = 0; - freeaddrinfo(res); - if (setsockopt(socket, IPPROTO_IPV6, IPV6_JOIN_GROUP, (char *)&mreq6, sizeof(mreq6)) == SOCKET_ERROR) - throw webcraft::async::detail::windows::overlapped_winsock2_runtime_error("Failed to join IPv6 multicast group"); - return; - } - freeaddrinfo(res); - throw std::invalid_argument("Invalid multicast address: " + group.host); - } - - void leave_group(const webcraft::async::io::socket::multicast_group &group) override - { - if (socket == INVALID_SOCKET) return; - struct addrinfo hints{}; - hints.ai_family = AF_UNSPEC; - hints.ai_flags = AI_NUMERICHOST; - struct addrinfo *res = nullptr; - if (getaddrinfo(group.host.c_str(), nullptr, &hints, &res) != 0 || !res) return; - if (res->ai_family == AF_INET) - { - auto *sa = (struct sockaddr_in *)res->ai_addr; - ip_mreq mreq{}; - mreq.imr_multiaddr = sa->sin_addr; - mreq.imr_interface.s_addr = INADDR_ANY; - freeaddrinfo(res); - setsockopt(socket, IPPROTO_IP, IP_DROP_MEMBERSHIP, (char *)&mreq, sizeof(mreq)); - return; - } - if (res->ai_family == AF_INET6) - { - auto *sa = (struct sockaddr_in6 *)res->ai_addr; - ipv6_mreq mreq6{}; - mreq6.ipv6mr_multiaddr = sa->sin6_addr; - mreq6.ipv6mr_interface = 0; - freeaddrinfo(res); - setsockopt(socket, IPPROTO_IPV6, IPV6_LEAVE_GROUP, (char *)&mreq6, sizeof(mreq6)); - } - else - freeaddrinfo(res); - } }; std::shared_ptr webcraft::async::io::socket::detail::make_udp_socket_descriptor(std::optional version) @@ -838,100 +760,42 @@ class io_uring_udp_socket_descriptor : public webcraft::async::io::socket::detai co_return event.get_result(); } - task sendto(std::span buffer, const webcraft::async::io::socket::connection_info &info) override - { - if (closed) - co_return 0; - - int socket = this->socket; - int bytes_sent = -1; - - std::function(sockaddr *, socklen_t)> async_func = [&bytes_sent, buffer, socket](sockaddr *addr, socklen_t len) -> task - { - auto event = webcraft::async::detail::as_awaitable( - webcraft::async::detail::linux::create_io_uring_event( - [socket, buffer, addr, len](struct io_uring_sqe *sqe) - { - io_uring_prep_sendto(sqe, socket, buffer.data(), buffer.size(), 0, addr, len); - })); + task sendto(std::span buffer, const webcraft::async::io::socket::connection_info &info) override + { + if (closed) + co_return 0; - co_await event; + int socket = this->socket; + int bytes_sent = -1; - bytes_sent = event.get_result(); + std::function(sockaddr *, socklen_t)> async_func = [&bytes_sent, buffer, socket](sockaddr *addr, socklen_t len) -> task + { + auto event = webcraft::async::detail::as_awaitable( + webcraft::async::detail::linux::create_io_uring_event( + [socket, buffer, addr, len](struct io_uring_sqe *sqe) + { + io_uring_prep_sendto(sqe, socket, buffer.data(), buffer.size(), 0, addr, len); + })); - co_return bytes_sent >= 0; - }; + co_await event; - bool flag = co_await async_host_port_to_addr( - info, async_func); + bytes_sent = event.get_result(); - if (!flag) - { - std::error_code ec(errno, std::system_category()); - throw std::system_error(ec, "Failed to send data"); - } + co_return bytes_sent >= 0; + }; - co_return bytes_sent; - } + bool flag = co_await async_host_port_to_addr( + info, async_func); - void join_group(const webcraft::async::io::socket::multicast_group &group, const webcraft::async::io::socket::multicast_join_options &) override + if (!flag) { - if (socket < 0) return; - in_addr maddr4; - if (inet_pton(AF_INET, group.host.c_str(), &maddr4) == 1) - { - if (!IN_MULTICAST(ntohl(maddr4.s_addr))) - throw std::invalid_argument("Not a multicast address: " + group.host); - struct ip_mreq mreq{}; - mreq.imr_multiaddr = maddr4; - mreq.imr_interface.s_addr = INADDR_ANY; - if (setsockopt(socket, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) - { - std::error_code ec(errno, std::system_category()); - throw std::system_error(ec, "Failed to join IPv4 multicast group"); - } - return; - } - struct in6_addr maddr6; - if (inet_pton(AF_INET6, group.host.c_str(), &maddr6) == 1) - { - if (!IN6_IS_ADDR_MULTICAST(&maddr6)) - throw std::invalid_argument("Not a multicast address: " + group.host); - struct ipv6_mreq mreq6{}; - mreq6.ipv6mr_multiaddr = maddr6; - mreq6.ipv6mr_interface = 0; - if (setsockopt(socket, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq6, sizeof(mreq6)) < 0) - { - std::error_code ec(errno, std::system_category()); - throw std::system_error(ec, "Failed to join IPv6 multicast group"); - } - return; - } - throw std::invalid_argument("Invalid multicast address: " + group.host); + std::error_code ec(errno, std::system_category()); + throw std::system_error(ec, "Failed to send data"); } - void leave_group(const webcraft::async::io::socket::multicast_group &group) override - { - if (socket < 0) return; - in_addr maddr4; - if (inet_pton(AF_INET, group.host.c_str(), &maddr4) == 1) - { - struct ip_mreq mreq{}; - mreq.imr_multiaddr = maddr4; - mreq.imr_interface.s_addr = INADDR_ANY; - setsockopt(socket, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq)); - return; - } - struct in6_addr maddr6; - if (inet_pton(AF_INET6, group.host.c_str(), &maddr6) == 1) - { - struct ipv6_mreq mreq6{}; - mreq6.ipv6mr_multiaddr = maddr6; - mreq6.ipv6mr_interface = 0; - setsockopt(socket, IPPROTO_IPV6, IPV6_LEAVE_GROUP, &mreq6, sizeof(mreq6)); - } - } - }; + co_return bytes_sent; + } +}; std::shared_ptr webcraft::async::io::socket::detail::make_udp_socket_descriptor(std::optional version) { @@ -974,26 +838,6 @@ class kqueue_udp_socket_descriptor : public webcraft::async::io::socket::detail: throw std::system_error(ec, "Failed to create UDP socket"); } - // Enable multicast loopback on macOS so that send/receive on the same host - // works (e.g. TestMulticastSendReceive). Without this, multicast packets - // sent by the sender are not delivered to local receivers. - int one = 1; -#if defined(SO_DOMAIN) - int domain = 0; - socklen_t domain_len = sizeof(domain); - if (::getsockopt(socket, SOL_SOCKET, SO_DOMAIN, &domain, &domain_len) == 0) - { - if (domain == AF_INET) - ::setsockopt(socket, IPPROTO_IP, IP_MULTICAST_LOOP, &one, sizeof(one)); - else if (domain == AF_INET6) - ::setsockopt(socket, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &one, sizeof(one)); - } -#else - // Fallback: set both IPv4 and IPv6 loop (only one will apply to the socket) - ::setsockopt(socket, IPPROTO_IP, IP_MULTICAST_LOOP, &one, sizeof(one)); - ::setsockopt(socket, IPPROTO_IPV6, IPV6_MULTICAST_LOOP, &one, sizeof(one)); -#endif - register_with_queue(); } } @@ -1231,64 +1075,6 @@ class kqueue_udp_socket_descriptor : public webcraft::async::io::socket::detail: read_event.notify(); } } - - void join_group(const webcraft::async::io::socket::multicast_group &group, const webcraft::async::io::socket::multicast_join_options &) override - { - if (socket < 0) return; - in_addr maddr4; - if (inet_pton(AF_INET, group.host.c_str(), &maddr4) == 1) - { - if (!IN_MULTICAST(ntohl(maddr4.s_addr))) - throw std::invalid_argument("Not a multicast address: " + group.host); - struct ip_mreq mreq{}; - mreq.imr_multiaddr = maddr4; - mreq.imr_interface.s_addr = INADDR_ANY; - if (setsockopt(socket, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0) - { - std::error_code ec(errno, std::system_category()); - throw std::system_error(ec, "Failed to join IPv4 multicast group"); - } - return; - } - struct in6_addr maddr6; - if (inet_pton(AF_INET6, group.host.c_str(), &maddr6) == 1) - { - if (!IN6_IS_ADDR_MULTICAST(&maddr6)) - throw std::invalid_argument("Not a multicast address: " + group.host); - struct ipv6_mreq mreq6{}; - mreq6.ipv6mr_multiaddr = maddr6; - mreq6.ipv6mr_interface = 0; - if (setsockopt(socket, IPPROTO_IPV6, IPV6_JOIN_GROUP, &mreq6, sizeof(mreq6)) < 0) - { - std::error_code ec(errno, std::system_category()); - throw std::system_error(ec, "Failed to join IPv6 multicast group"); - } - return; - } - throw std::invalid_argument("Invalid multicast address: " + group.host); - } - - void leave_group(const webcraft::async::io::socket::multicast_group &group) override - { - if (socket < 0) return; - in_addr maddr4; - if (inet_pton(AF_INET, group.host.c_str(), &maddr4) == 1) - { - struct ip_mreq mreq{}; - mreq.imr_multiaddr = maddr4; - mreq.imr_interface.s_addr = INADDR_ANY; - setsockopt(socket, IPPROTO_IP, IP_DROP_MEMBERSHIP, &mreq, sizeof(mreq)); - return; - } - struct in6_addr maddr6; - if (inet_pton(AF_INET6, group.host.c_str(), &maddr6) == 1) - { - struct ipv6_mreq mreq6{}; - mreq6.ipv6mr_multiaddr = maddr6; - mreq6.ipv6mr_interface = 0; - setsockopt(socket, IPPROTO_IPV6, IPV6_LEAVE_GROUP, &mreq6, sizeof(mreq6)); - } - } }; std::shared_ptr webcraft::async::io::socket::detail::make_udp_socket_descriptor(std::optional version) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 96693dc..1ec674c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,13 +8,6 @@ add_executable(webcraft_tests ${TEST_SOURCES}) target_include_directories(webcraft_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include) target_link_libraries(webcraft_tests PRIVATE WebCraft GTest::gtest GTest::gtest_main) -# Allow tests to skip multicast-dependent cases when multicast is not supported. -if(WEBCRAFT_HAS_MULTICAST) - target_compile_definitions(webcraft_tests PRIVATE WEBCRAFT_HAS_MULTICAST=1) -else() - target_compile_definitions(webcraft_tests PRIVATE WEBCRAFT_HAS_MULTICAST=0) -endif() - include(GoogleTest) gtest_discover_tests(webcraft_tests DISCOVERY_TIMEOUT 30 diff --git a/tests/src/test_async_io_multicast.cpp b/tests/src/test_async_io_multicast.cpp deleted file mode 100644 index 840a3ae..0000000 --- a/tests/src/test_async_io_multicast.cpp +++ /dev/null @@ -1,189 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////// -// Copyright (c) Aditya Rao -// Licenced under MIT license. See LICENSE.txt for details. -/////////////////////////////////////////////////////////////////////////////// - -#define TEST_SUITE_NAME MulticastSocketTestSuite - -#ifndef WEBCRAFT_HAS_MULTICAST -#define WEBCRAFT_HAS_MULTICAST 1 -#endif - -#include "test_suite.hpp" -#include -#include -#include -#include -#include - -using namespace webcraft::async; -using namespace webcraft::async::io::socket; - -namespace -{ - constexpr uint16_t multicast_port = 19000; - const std::string multicast_addr = "239.255.0.1"; - const connection_info bind_info = {"0.0.0.0", multicast_port}; - const ip_version version = ip_version::IPv4; -} // namespace - -TEST_CASE(TestMulticastGroupResolve) -{ - auto group = multicast_group::resolve(multicast_addr); - EXPECT_EQ(group.host, multicast_addr); - EXPECT_EQ(group.port, 0u); - - group.port = multicast_port; - EXPECT_EQ(group.port, multicast_port); -} - -#if WEBCRAFT_HAS_MULTICAST -TEST_CASE(TestMulticastJoinLeave) -{ - runtime_context context; - - auto task_fn = co_async - { - multicast_socket socket = make_multicast_socket(version); - socket.bind(bind_info); - - multicast_group group = multicast_group::resolve(multicast_addr); - group.port = multicast_port; - - EXPECT_NO_THROW(socket.join(group)); - EXPECT_NO_THROW(socket.leave(group)); - - co_await socket.close(); - }; - - sync_wait(task_fn()); -} -#else -TEST_CASE(TestMulticastJoinLeave) -{ - GTEST_SKIP() << "Multicast not supported (WEBCRAFT_HAS_MULTICAST=0)"; -} -#endif - -#if WEBCRAFT_HAS_MULTICAST -TEST_CASE(TestMulticastInvalidAddressThrows) -{ - runtime_context context; - - auto task_fn = co_async - { - multicast_socket socket = make_multicast_socket(version); - socket.bind(bind_info); - - // Resolve with an invalid/non-multicast string; join may throw std::invalid_argument or std::system_error - multicast_group invalid_group = multicast_group::resolve("not.an.ip.address"); - invalid_group.port = multicast_port; - -#if !defined(WEBCRAFT_UDP_MOCK) - EXPECT_THROW(socket.join(invalid_group), std::exception); -#endif - - co_await socket.close(); - }; - - sync_wait(task_fn()); -} -#else -TEST_CASE(TestMulticastInvalidAddressThrows) -{ - GTEST_SKIP() << "Multicast not supported (WEBCRAFT_HAS_MULTICAST=0)"; -} -#endif - -#if WEBCRAFT_HAS_MULTICAST -TEST_CASE(TestMulticastSendReceive) -{ - runtime_context context; - - multicast_group group = multicast_group::resolve(multicast_addr); - group.port = multicast_port; - - const std::string message = "Hello, multicast!"; - std::atomic received{false}; - std::string received_data; - - auto receiver_fn = co_async - { - multicast_socket recv_socket = make_multicast_socket(version); - recv_socket.bind(bind_info); - recv_socket.join(group); - - std::vector buffer(1024); - connection_info sender_info{}; - size_t n = co_await recv_socket.recvfrom(std::span(buffer.data(), buffer.size()), sender_info); - if (n > 0) - { - received_data.assign(buffer.data(), n); - received = true; - } - - recv_socket.leave(group); - co_await recv_socket.close(); - }; - - auto sender_fn = co_async - { - multicast_socket send_socket = make_multicast_socket(version); - size_t n = co_await send_socket.sendto(std::span(message.data(), message.size()), group); - EXPECT_EQ(n, message.size()); - co_await send_socket.close(); - }; - - auto recv_task = receiver_fn(); - auto send_task = sender_fn(); - - // Run receiver in background so it is in recvfrom before we send; then run sender on main thread. - std::thread recv_thread([&]() { sync_wait(recv_task); }); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - sync_wait(send_task); - recv_thread.join(); - - if (!received) - { - GTEST_SKIP() << "Multicast loopback not available in this environment (e.g. some macOS CI runners)"; - } - EXPECT_EQ(received_data, message) << "Received data should match sent message"; -} -#else -TEST_CASE(TestMulticastSendReceive) -{ - GTEST_SKIP() << "Multicast not supported (WEBCRAFT_HAS_MULTICAST=0)"; -} -#endif - -#if WEBCRAFT_HAS_MULTICAST -TEST_CASE(TestMulticastJoinLeaveMultipleGroups) -{ - runtime_context context; - - auto task_fn = co_async - { - multicast_socket socket = make_multicast_socket(version); - socket.bind(bind_info); - - auto group1 = multicast_group::resolve("239.255.0.1"); - group1.port = multicast_port; - auto group2 = multicast_group::resolve("239.255.0.2"); - group2.port = multicast_port; - - socket.join(group1); - socket.join(group2); - socket.leave(group1); - socket.leave(group2); - - co_await socket.close(); - }; - - sync_wait(task_fn()); -} -#else -TEST_CASE(TestMulticastJoinLeaveMultipleGroups) -{ - GTEST_SKIP() << "Multicast not supported (WEBCRAFT_HAS_MULTICAST=0)"; -} -#endif