azkoyen_technical_test/tests/test_unix_ipc.cxx

221 lines
6.0 KiB
C++

#include <gtest/gtest.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <atomic>
#include <chrono>
#include <cstring>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
#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<int>& 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<sockaddr*>(&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<ssize_t>(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<bool> m_ready{false};
std::thread m_thread;
std::vector<int> 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<int>::max());
consumer.join();
ASSERT_EQ(consumer.received().size(), 1u);
EXPECT_EQ(consumer.received()[0], std::numeric_limits<int>::max());
}
{
const std::string sock = "/tmp/test_ipc_min.sock";
FakeConsumer consumer(sock, 1);
consumer.start();
UnixIpcBridge bridge(sock);
bridge.send(std::numeric_limits<int>::min());
consumer.join();
ASSERT_EQ(consumer.received().size(), 1u);
EXPECT_EQ(consumer.received()[0], std::numeric_limits<int>::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<int> 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<size_t>(kMessages));
for (int i = 0; i < kMessages; ++i)
{
EXPECT_EQ(all_received[i], i * 10);
}
}