From a95d6a097b7d9a7d04ffc4cab60f381e85521e59 Mon Sep 17 00:00:00 2001 From: xyzconstant <263061129+xyzconstant@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:02:53 -0300 Subject: [PATCH 1/2] Extract `UnixListener` class to a dedicated file Next commit will consume the `UnixListener` class in another test file, so move it to a shared one. --- test/mp/test/listen_tests.cpp | 86 +++++------------------------------ test/mp/test/unixlistener.cpp | 71 +++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 75 deletions(-) create mode 100644 test/mp/test/unixlistener.cpp diff --git a/test/mp/test/listen_tests.cpp b/test/mp/test/listen_tests.cpp index 4e579d99..9380b8d4 100644 --- a/test/mp/test/listen_tests.cpp +++ b/test/mp/test/listen_tests.cpp @@ -2,34 +2,28 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. -#include -#include +#include "unixlistener.cpp" +#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 -#include // IWYU pragma: keep #include +#include // IWYU pragma: keep #include -#include -#include -#include #include -#include namespace mp { namespace test { @@ -37,64 +31,6 @@ namespace { constexpr auto FAILURE_TIMEOUT = std::chrono::seconds{30}; -//! Owns a temporary Unix-domain listening socket used by ListenSetup. Tests call -//! Connect() to create client socket FDs and release() to transfer the listening -//! FD to ListenConnections(). -class UnixListener -{ -public: - UnixListener() - { - std::string dir_template = (std::filesystem::temp_directory_path() / "mptest-listener-XXXXXX").string(); - char* dir = mkdtemp(dir_template.data()); - KJ_REQUIRE(dir != nullptr); - m_dir = dir; - m_path = m_dir + "/socket"; - - m_fd = socket(AF_UNIX, SOCK_STREAM, 0); - KJ_REQUIRE(m_fd >= 0); - - sockaddr_un addr{}; - addr.sun_family = AF_UNIX; - KJ_REQUIRE(m_path.size() < sizeof(addr.sun_path)); - std::strncpy(addr.sun_path, m_path.c_str(), sizeof(addr.sun_path) - 1); - KJ_REQUIRE(bind(m_fd, reinterpret_cast(&addr), sizeof(addr)) == 0); - KJ_REQUIRE(listen(m_fd, SOMAXCONN) == 0); - } - - ~UnixListener() - { - if (m_fd >= 0) close(m_fd); - if (!m_path.empty()) unlink(m_path.c_str()); - if (!m_dir.empty()) rmdir(m_dir.c_str()); - } - - int release() - { - assert(m_fd >= 0); - int fd = m_fd; - m_fd = -1; - return fd; - } - - int MakeConnectedSocket() const - { - int fd = socket(AF_UNIX, SOCK_STREAM, 0); - KJ_REQUIRE(fd >= 0); - - sockaddr_un addr{}; - addr.sun_family = AF_UNIX; - KJ_REQUIRE(m_path.size() < sizeof(addr.sun_path)); - std::strncpy(addr.sun_path, m_path.c_str(), sizeof(addr.sun_path) - 1); - KJ_REQUIRE(connect(fd, reinterpret_cast(&addr), sizeof(addr)) == 0); - return fd; - } - -private: - int m_fd{-1}; - std::string m_dir; - std::string m_path; -}; //! Runs a client EventLoop on its own thread and connects one socket FD to the //! server. The constructed ProxyClient can be used by the test thread to make diff --git a/test/mp/test/unixlistener.cpp b/test/mp/test/unixlistener.cpp new file mode 100644 index 00000000..6523a3bd --- /dev/null +++ b/test/mp/test/unixlistener.cpp @@ -0,0 +1,71 @@ +// Copyright (c) The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include +#include +#include +#include + +#include +#include +#include + +//! Owns a temporary Unix-domain listening socket used by ListenSetup. Tests call +//! Connect() to create client socket FDs and release() to transfer the listening +//! FD to ListenConnections(). +class UnixListener +{ +public: + UnixListener() + { + std::string dir_template = (std::filesystem::temp_directory_path() / "mptest-listener-XXXXXX").string(); + char* dir = mkdtemp(dir_template.data()); + KJ_REQUIRE(dir != nullptr); + m_dir = dir; + m_path = m_dir + "/socket"; + + m_fd = socket(AF_UNIX, SOCK_STREAM, 0); + KJ_REQUIRE(m_fd >= 0); + + sockaddr_un addr{}; + addr.sun_family = AF_UNIX; + KJ_REQUIRE(m_path.size() < sizeof(addr.sun_path)); + std::strncpy(addr.sun_path, m_path.c_str(), sizeof(addr.sun_path) - 1); + KJ_REQUIRE(bind(m_fd, reinterpret_cast(&addr), sizeof(addr)) == 0); + KJ_REQUIRE(listen(m_fd, SOMAXCONN) == 0); + } + + ~UnixListener() + { + if (m_fd >= 0) close(m_fd); + if (!m_path.empty()) unlink(m_path.c_str()); + if (!m_dir.empty()) rmdir(m_dir.c_str()); + } + + int release() + { + assert(m_fd >= 0); + int fd = m_fd; + m_fd = -1; + return fd; + } + + int MakeConnectedSocket() const + { + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + KJ_REQUIRE(fd >= 0); + + sockaddr_un addr{}; + addr.sun_family = AF_UNIX; + KJ_REQUIRE(m_path.size() < sizeof(addr.sun_path)); + std::strncpy(addr.sun_path, m_path.c_str(), sizeof(addr.sun_path) - 1); + KJ_REQUIRE(connect(fd, reinterpret_cast(&addr), sizeof(addr)) == 0); + return fd; + } + +private: + int m_fd{-1}; + std::string m_dir; + std::string m_path; +}; From bd3c83dcb8882cdc211eea546d780c7304fdd2c8 Mon Sep 17 00:00:00 2001 From: xyzconstant <263061129+xyzconstant@users.noreply.github.com> Date: Tue, 23 Jun 2026 22:49:13 -0300 Subject: [PATCH 2/2] Add test coverage for ConnectStream This commit introduces a new test file `connect_tests.cpp` for testing the `ConnectStream` client method. --- test/CMakeLists.txt | 1 + test/mp/test/connect_tests.cpp | 182 +++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 test/mp/test/connect_tests.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 13246293..fd0aa023 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -28,6 +28,7 @@ if(BUILD_TESTING AND TARGET CapnProto::kj-test) mp/test/foo.h mp/test/listen_tests.cpp mp/test/spawn_tests.cpp + mp/test/connect_tests.cpp mp/test/test.cpp ) include(${PROJECT_SOURCE_DIR}/cmake/TargetCapnpSources.cmake) diff --git a/test/mp/test/connect_tests.cpp b/test/mp/test/connect_tests.cpp new file mode 100644 index 00000000..fff51996 --- /dev/null +++ b/test/mp/test/connect_tests.cpp @@ -0,0 +1,182 @@ +// Copyright (c) The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://opensource.org. +#include "unixlistener.cpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include // IWYU pragma: keep +#include +#include +#include +#include +#include +#include +#include + +namespace mp { +namespace test { +namespace { + +class TestSetup +{ +public: + int client_fd; + int server_fd; + + mp::EventLoop* loop; + //! Thread variable should be after other struct members so the thread does + //! not start until the other members are initialized. + std::thread loop_thread; + + TestSetup() : TestSetup([](int fds[2]) { + KJ_REQUIRE(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != -1); + }) {} + + TestSetup(const std::function& init_sockets) + { + std::promise loop_promise; + loop_thread = std::thread([&] { + mp::EventLoop loop("mptest-connect", [](mp::LogMessage log) { + if (log.level == mp::Log::Raise) + throw std::runtime_error(log.message); + }); + loop_promise.set_value(&loop); + loop.loop(); + }); + loop = loop_promise.get_future().get(); + + // Initalize and store sockets + int fds[2] = {-1, -1}; + init_sockets(fds); + + client_fd = fds[0]; + server_fd = fds[1]; + } + + ~TestSetup() + { + loop_thread.join(); + } +}; + +KJ_TEST("Successfully connects to a socket serving a valid init interface") +{ + TestSetup setup; + + std::thread server_thread([&setup]() { + mp::EventLoop server_loop("mptest-valid-server", [](mp::LogMessage log) { + if (log.level == mp::Log::Raise) + throw std::runtime_error(log.message); + }); + std::unique_ptr init = std::make_unique(); + ServeStream(server_loop, setup.server_fd, *init); + server_loop.loop(); + }); + + auto init = ConnectStream(*setup.loop, setup.client_fd); + init->initThreadMap(); + + init.reset(); + server_thread.join(); + KJ_EXPECT(true); +} + +KJ_TEST("Passing a disconnected socket will throw") +{ + TestSetup setup; + + close(setup.server_fd); + + try { + auto init = ConnectStream(*setup.loop, setup.client_fd); + init->initThreadMap(); + KJ_EXPECT(false); + } catch (const std::runtime_error& e) { + std::string_view reason = e.what(); + + KJ_EXPECT(reason == "IPC client method called after disconnect."); + } +} + +KJ_TEST("Passing a connected socket that disconnects on recv will throw") +{ + TestSetup setup; + + std::thread server_thread([&setup]() { + char buf[128]; + + ssize_t bytes_received = + recv(setup.server_fd, buf, sizeof(buf), 0); + + if (bytes_received > 0) { + close(setup.server_fd); + } + }); + + try { + auto init = ConnectStream(*setup.loop, setup.client_fd); + init->initThreadMap(); + + if (server_thread.joinable()) server_thread.join(); + KJ_EXPECT(false); + } catch (const std::runtime_error& e) { + if (server_thread.joinable()) server_thread.join(); + + std::string_view reason = e.what(); + KJ_EXPECT(reason == "IPC client method called after disconnect." || reason == "IPC client method call interrupted by disconnect."); + } +} + + +KJ_TEST("Passing a custom listening socket that disconnects on recv will throw") +{ + UnixListener listener; + + TestSetup setup([&listener](int fds[2]) { + fds[0] = listener.MakeConnectedSocket(); // client_fd + fds[1] = listener.release(); // server_fd + }); + + std::thread server_thread([&setup]() { + char buf[128]; + + int connection_fd = accept(setup.server_fd, nullptr, nullptr); + + if (connection_fd >= 0) { + ssize_t bytes_received = + recv(connection_fd, buf, sizeof(buf), 0); + + if (bytes_received > 0) { + close(connection_fd); + } + } + close(setup.server_fd); + }); + + try { + auto init = ConnectStream(*setup.loop, setup.client_fd); + init->initThreadMap(); + + if (server_thread.joinable()) server_thread.join(); + KJ_EXPECT(false); + } catch (const std::runtime_error& e) { + if (server_thread.joinable()) server_thread.join(); + + std::string_view reason = e.what(); + KJ_EXPECT(reason == "IPC client method called after disconnect." || reason == "IPC client method call interrupted by disconnect."); + } +} + +} // namespace +} // namespace test +} // namespace mp