diff --git a/CMakeLists.txt b/CMakeLists.txt index 891bfc2..682d705 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # Core library add_library(core src/core/SysfsRead.cxx + src/core/UnixIpcBridge.cxx src/core/Producer.cxx ) diff --git a/README.md b/README.md index f334dd7..8859ddb 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,12 @@ The reader never throws on I/O errors; every outcome is expressed through the en ## Producer class / thread `Producer` ([include/Producer.hpp](include/Producer.hpp), [src/core/Producer.cxx](src/core/Producer.cxx)) runs a worker `std::thread` that periodically polls the `SysfsReader` and, when the status is `Enabled`, generates a random integer and forwards it through an injected `send_fn` callback. The polling interval is 1 second under normal conditions and 7 seconds when the sysfs file reports `ErrorTempTooHigh` (cool-down). + +## UnixIpcBridge + +>[!note] +>why unix domain sockets? Because I have more experience with them under linux than with posix shared memmory and semaphore, and I find them easier to unit-test. + +`UnixIpcBridge` ([include/UnixIpcBridge.hpp](include/UnixIpcBridge.hpp), [src/core/UnixIpcBridge.cxx](src/core/UnixIpcBridge.cxx)) is a small helper that connects to a UNIX domain socket and sends a single `int` per call. It opens a new connection for each value, which keeps the protocol stateless and simple. + +**Tests:** [tests/test_unix_ipc.cxx](tests/test_unix_ipc.cxx) — spins up a fake socket server, sends values through the bridge, and asserts they arrive correctly. diff --git a/include/UnixIpcBridge.hpp b/include/UnixIpcBridge.hpp new file mode 100644 index 0000000..24da3e8 --- /dev/null +++ b/include/UnixIpcBridge.hpp @@ -0,0 +1,31 @@ +// UnixIpcBridge.hpp +// SPDX-License-Identifier: GPL-3.0-only +// Author: Unai Blazquez + +#pragma once +#include // non‑blocking +#include +#include +#include // close() + +#include + +///@brief Small bridge to allow the producer class to send data over UNIX domain +/// sockets +class UnixIpcBridge +{ + public: + ///@brief constructor + ///@param socket path pointing the socket + explicit UnixIpcBridge(const std::string& socket_path); + + ///@brief sending function, this goes into the producer + ///@param integer to send over the socket + void send(int value); + + private: + std::string m_socket_path; + int m_socket_fd = -1; + + void connect_to_consumer(); +}; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 82e7429..981d9c8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -25,3 +25,15 @@ target_link_libraries(test_producer add_test(NAME test_producer COMMAND test_producer) +add_executable(test_ipc + test_unix_ipc.cxx +) + +target_link_libraries(test_ipc + PRIVATE + core + gtest + gtest_main +) + +add_test(NAME test_ipc COMMAND test_ipc) diff --git a/tests/test_unix_ipc.cxx b/tests/test_unix_ipc.cxx new file mode 100644 index 0000000..c244ded --- /dev/null +++ b/tests/test_unix_ipc.cxx @@ -0,0 +1,220 @@ +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "UnixIpcBridge.hpp" + +// --------------------------------------------------------------------------- +// Helper: a minimal UNIX-domain socket server that accepts one connection, +// reads `count` ints, then tears down cleanly. Uses a ready-flag so the +// client never races against bind/listen. +// --------------------------------------------------------------------------- +class FakeConsumer +{ + public: + explicit FakeConsumer(const std::string& path, int count = 1) + : m_path(path), m_count(count) + { + // Remove stale socket from any previous failed run + unlink(m_path.c_str()); + } + + /// Start the server on a background thread. + void start() + { + m_thread = std::thread([this] { run(); }); + + // Spin until the server signals it is listening (bounded wait). + auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5); + while (!m_ready.load(std::memory_order_acquire)) + { + if (std::chrono::steady_clock::now() > deadline) + { + throw std::runtime_error("FakeConsumer: server failed to start"); + } + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + + /// Block until the server thread finishes. + void join() { m_thread.join(); } + + /// Values received in order. + const std::vector& received() const { return m_received; } + + ~FakeConsumer() { unlink(m_path.c_str()); } + + private: + void run() + { + m_server_fd = socket(AF_UNIX, SOCK_STREAM, 0); + ASSERT_GE(m_server_fd, 0) << "socket() failed: " << strerror(errno); + + struct sockaddr_un addr = {}; + addr.sun_family = AF_UNIX; + std::strncpy(addr.sun_path, m_path.c_str(), sizeof(addr.sun_path) - 1); + + ASSERT_EQ( + bind(m_server_fd, reinterpret_cast(&addr), sizeof(addr)), 0) + << "bind() failed: " << strerror(errno); + ASSERT_EQ(listen(m_server_fd, 1), 0) + << "listen() failed: " << strerror(errno); + + // Signal that we are ready to accept connections. + m_ready.store(true, std::memory_order_release); + + int client_fd = accept(m_server_fd, nullptr, nullptr); + ASSERT_GE(client_fd, 0) << "accept() failed: " << strerror(errno); + + for (int i = 0; i < m_count; ++i) + { + int value = 0; + ssize_t n = recv(client_fd, &value, sizeof(value), MSG_WAITALL); + ASSERT_EQ(n, static_cast(sizeof(value))) + << "recv() short read on message " << i; + m_received.push_back(value); + } + + close(client_fd); + close(m_server_fd); + } + + std::string m_path; + int m_count; + int m_server_fd = -1; + std::atomic m_ready{false}; + std::thread m_thread; + std::vector m_received; +}; + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +/// Sends a single integer value and verifies the consumer receives it. +TEST(UnixIpcBridgeTest, SendsSingleInt) +{ + const std::string sock = "/tmp/test_ipc_single.sock"; + + FakeConsumer consumer(sock, /*count=*/1); + consumer.start(); + + UnixIpcBridge bridge(sock); + bridge.send(42); + + consumer.join(); + + ASSERT_EQ(consumer.received().size(), 1u); + EXPECT_EQ(consumer.received()[0], 42); +} + +/// Sends zero and a negative value — makes sure sign bits survive. +TEST(UnixIpcBridgeTest, SendsZeroAndNegativeValues) +{ + // Zero + { + const std::string sock = "/tmp/test_ipc_zero.sock"; + FakeConsumer consumer(sock, 1); + consumer.start(); + + UnixIpcBridge bridge(sock); + bridge.send(0); + + consumer.join(); + ASSERT_EQ(consumer.received().size(), 1u); + EXPECT_EQ(consumer.received()[0], 0); + } + + // Negative + { + const std::string sock = "/tmp/test_ipc_neg.sock"; + FakeConsumer consumer(sock, 1); + consumer.start(); + + UnixIpcBridge bridge(sock); + bridge.send(-1); + + consumer.join(); + ASSERT_EQ(consumer.received().size(), 1u); + EXPECT_EQ(consumer.received()[0], -1); + } +} + +/// Sends INT_MAX / INT_MIN to check for truncation or overflow. +TEST(UnixIpcBridgeTest, SendsExtremeBoundaryValues) +{ + { + const std::string sock = "/tmp/test_ipc_max.sock"; + FakeConsumer consumer(sock, 1); + consumer.start(); + + UnixIpcBridge bridge(sock); + bridge.send(std::numeric_limits::max()); + + consumer.join(); + ASSERT_EQ(consumer.received().size(), 1u); + EXPECT_EQ(consumer.received()[0], std::numeric_limits::max()); + } + + { + const std::string sock = "/tmp/test_ipc_min.sock"; + FakeConsumer consumer(sock, 1); + consumer.start(); + + UnixIpcBridge bridge(sock); + bridge.send(std::numeric_limits::min()); + + consumer.join(); + ASSERT_EQ(consumer.received().size(), 1u); + EXPECT_EQ(consumer.received()[0], std::numeric_limits::min()); + } +} + +/// Connecting to a non-existent socket must throw, not silently fail. +TEST(UnixIpcBridgeTest, ThrowsWhenNoConsumerListening) +{ + const std::string sock = "/tmp/test_ipc_noserver.sock"; + unlink(sock.c_str()); // make sure nothing is there + + UnixIpcBridge bridge(sock); + EXPECT_THROW(bridge.send(99), std::runtime_error); +} + +/// Multiple sequential sends (each reopens the connection). +TEST(UnixIpcBridgeTest, MultipleSendsSequentially) +{ + const std::string sock = "/tmp/test_ipc_multi.sock"; + constexpr int kMessages = 5; + + // Server expects exactly kMessages ints from kMessages connections. + // Because the bridge reconnects every send(), we run kMessages + // single-message consumers sequentially. + std::vector all_received; + for (int i = 0; i < kMessages; ++i) + { + FakeConsumer consumer(sock, 1); + consumer.start(); + + UnixIpcBridge bridge(sock); + bridge.send(i * 10); + + consumer.join(); + ASSERT_EQ(consumer.received().size(), 1u); + all_received.push_back(consumer.received()[0]); + } + + ASSERT_EQ(all_received.size(), static_cast(kMessages)); + for (int i = 0; i < kMessages; ++i) + { + EXPECT_EQ(all_received[i], i * 10); + } +}