cheatah
Source

stdlib/tests/socket_test.cpp

1// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).
2// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.
3#include "socket.hpp"
5#include <chrono>
6#include <string>
7#include <thread>
8#include <utility>
10#if !defined(_WIN32)
11#include <netinet/in.h>
12#include <netinet/tcp.h>
13#include <sys/socket.h>
14#endif
16#include <gtest/gtest.h>
18namespace sk = cheatah::socket;
20// A real loopback round-trip: a listener accepts one client, reads its request,
21// and replies; a client thread connects, sends, and reads the reply. Exercises
22// tcp_listen / local_port / accept / recv / sendall / tcp_connect / send / close.
23TEST(CheatahSocket, Loopback) {
24 const long long lfd = sk::tcp_listen("127.0.0.1", 0, 4);
25 ASSERT_GE(lfd, 0) << sk::last_error();
26 const long long port = sk::local_port(lfd);
27 ASSERT_GT(port, 0);
29 std::string client_reply;
30 std::thread client([&] {
31 const long long cfd = sk::tcp_connect("127.0.0.1", port);
32 ASSERT_GE(cfd, 0);
33 ASSERT_EQ(sk::sendall(cfd, "ping"), 0);
34 client_reply = sk::recv(cfd, 64);
35 sk::close(cfd);
36 });
38 const long long conn = sk::accept(lfd);
39 ASSERT_GE(conn, 0) << sk::last_error();
40 EXPECT_EQ(sk::recv(conn, 64), "ping");
41 EXPECT_EQ(sk::sendall(conn, "pong"), 0);
42 sk::close(conn);
44 client.join();
45 EXPECT_EQ(client_reply, "pong");
46 sk::close(lfd);
49// A larger, binary-ish payload to drive send()/sendall() through multiple writes
50// and confirm byte-exactness (including an embedded NUL).
51TEST(CheatahSocket, Sendall) {
52 const long long lfd = sk::tcp_listen("127.0.0.1", 0, 1);
53 ASSERT_GE(lfd, 0);
54 const long long port = sk::local_port(lfd);
56 std::string payload(8000, 'x');
57 payload[100] = '\0';
58 payload[7999] = 'Z';
60 std::thread client([&] {
61 const long long cfd = sk::tcp_connect("127.0.0.1", port);
62 ASSERT_GE(cfd, 0);
63 EXPECT_GT(sk::send(cfd, "hi"), 0); // single send()
64 EXPECT_EQ(sk::sendall(cfd, payload), 0);
65 sk::close(cfd);
66 });
68 const long long conn = sk::accept(lfd);
69 ASSERT_GE(conn, 0);
70 std::string got;
71 while (got.size() < payload.size() + 2) {
72 const std::string chunk = sk::recv(conn, 4096);
73 if (chunk.empty()) break;
74 got += chunk;
75 }
76 EXPECT_EQ(got, "hi" + payload);
77 sk::close(conn);
78 client.join();
79 sk::close(lfd);
82#if !defined(_WIN32)
83// THE "it's the default for every user" guarantee: every connected stream socket — client AND
84// accepted server side — comes tuned for throughput (TCP_NODELAY on, an enlarged SO_RCVBUF)
85// WITHOUT any caller opt-in. This is the regression guard that a future refactor can't silently
86// drop the tuning that turns a 0.2 MB/s crawl into a full-speed download.
87TEST(CheatahSocket, ConnectedSocketsAreTunedByDefault) {
88 const long long lfd = sk::tcp_listen("127.0.0.1", 0, 1);
89 ASSERT_GE(lfd, 0) << sk::last_error();
90 const long long port = sk::local_port(lfd);
92 long long cfd = -1;
93 std::thread client([&] { cfd = sk::tcp_connect("127.0.0.1", port); });
94 const long long conn = sk::accept(lfd);
95 client.join();
96 ASSERT_GE(cfd, 0) << sk::last_error();
97 ASSERT_GE(conn, 0) << sk::last_error();
99 const auto nodelay = [](long long fd) {
100 int v = 0;
101 socklen_t len = sizeof(v);
102 EXPECT_EQ(::getsockopt(static_cast<int>(fd), IPPROTO_TCP, TCP_NODELAY, &v, &len), 0);
103 return v;
104 };
105 const auto rcvbuf = [](long long fd) {
106 int v = 0;
107 socklen_t len = sizeof(v);
108 EXPECT_EQ(::getsockopt(static_cast<int>(fd), SOL_SOCKET, SO_RCVBUF, &v, &len), 0);
109 return v; // Linux reports 2× the requested value
110 };
111 // Nagle is OFF on both ends.
112 EXPECT_NE(nodelay(cfd), 0) << "client socket left Nagle on";
113 EXPECT_NE(nodelay(conn), 0) << "accepted socket left Nagle on";
114 // The receive buffer is well above the kernel's stock default (131072 here) — proof the
115 // window was opened. Floor chosen below the requested 4 MiB to tolerate rmem_max clamping.
116 EXPECT_GE(rcvbuf(cfd), 512 * 1024) << "client SO_RCVBUF not enlarged";
117 EXPECT_GE(rcvbuf(conn), 512 * 1024) << "accepted SO_RCVBUF not enlarged";
119 sk::close(cfd);
120 sk::close(conn);
121 sk::close(lfd);
124// A multi-megabyte loopback transfer read back byte-for-byte, and fast — the socket-layer
125// throughput regression guard. If a change reverted the tuning or crippled recv, a several-MB
126// loopback transfer would still be correct but this asserts it also stays quick.
127TEST(CheatahSocket, BulkTransferIsCorrectAndPrompt) {
128 const long long lfd = sk::tcp_listen("127.0.0.1", 0, 1);
129 ASSERT_GE(lfd, 0);
130 const long long port = sk::local_port(lfd);
132 // 8 MiB with a position-dependent pattern so any misorder/truncation is caught.
133 std::string payload(8 * 1024 * 1024, '\0');
134 for (std::size_t i = 0; i < payload.size(); ++i) {
135 payload[i] = static_cast<char>((i * 1103515245u + 12345u) >> 16);
136 }
138 std::thread server([&] {
139 const long long conn = sk::accept(lfd);
140 ASSERT_GE(conn, 0);
141 EXPECT_EQ(sk::sendall(conn, payload), 0);
142 sk::close(conn);
143 });
145 const auto t0 = std::chrono::steady_clock::now();
146 const long long cfd = sk::tcp_connect("127.0.0.1", port);
147 ASSERT_GE(cfd, 0);
148 std::string got;
149 got.reserve(payload.size());
150 while (got.size() < payload.size()) {
151 const std::string chunk = sk::recv(cfd, 65536);
152 if (chunk.empty()) break;
153 got += chunk;
154 }
155 const double secs = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
156 sk::close(cfd);
157 server.join();
158 sk::close(lfd);
160 ASSERT_EQ(got.size(), payload.size());
161 EXPECT_EQ(got, payload);
162 // Loopback moves GB/s; 8 MiB in >5 s would mean the recv path regressed hard. Generous so CI
163 // load never flakes it.
164 EXPECT_LT(secs, 5.0) << "8 MiB loopback took " << secs << "s — recv path regressed";
166#endif // !_WIN32
168// The low-level BSD path: socket -> set_reuseaddr -> bind -> listen -> local_port.
169TEST(CheatahSocket, ListenLowLevel) {
170 const long long fd = sk::socket();
171 ASSERT_GE(fd, 0);
172 EXPECT_EQ(sk::set_reuseaddr(fd), 0);
173 EXPECT_EQ(sk::bind(fd, "127.0.0.1", 0), 0);
174 EXPECT_EQ(sk::listen(fd, 1), 0);
175 EXPECT_GT(sk::local_port(fd), 0);
176 EXPECT_EQ(sk::close(fd), 0);
179// Connecting to a port nobody is listening on fails, and last_error() is set.
180TEST(CheatahSocket, ConnectRefused) {
181 // Bind+listen to grab a free port, then close it so the port is free again.
182 const long long probe = sk::tcp_listen("127.0.0.1", 0, 1);
183 ASSERT_GE(probe, 0);
184 const long long dead_port = sk::local_port(probe);
185 sk::close(probe);
187 EXPECT_LT(sk::tcp_connect("127.0.0.1", dead_port), 0);
188 EXPECT_FALSE(sk::last_error().empty());
191// Name resolution failure is reported as -1, not a crash.
192TEST(CheatahSocket, ResolveFailure) {
193 const long long fd = sk::socket();
194 ASSERT_GE(fd, 0);
195 EXPECT_LT(sk::bind(fd, "no.such.host.invalid.", 0), 0);
196 EXPECT_LT(sk::tcp_connect("no.such.host.invalid.", 80), 0);
197 EXPECT_LT(sk::tcp_listen("no.such.host.invalid.", 0, 1), 0); // exercises tcp_listen's error path
198 sk::close(fd);
201// Operations on a bad fd return errors (and recv returns ""), never crash.
202TEST(CheatahSocket, BadFd) {
203 EXPECT_EQ(sk::recv(-1, 64), "");
204 EXPECT_EQ(sk::recv(5, 0), ""); // non-positive bufsize
205 EXPECT_LT(sk::send(-1, "x"), 0);
206 EXPECT_LT(sk::sendall(-1, "x"), 0);
207 EXPECT_LT(sk::listen(-1, 1), 0);
208 EXPECT_LT(sk::accept(-1), 0);
209 EXPECT_LT(sk::local_port(-1), 0);
210 EXPECT_LT(sk::connect(-1, "127.0.0.1", 80), 0);
211 EXPECT_LT(sk::close(-1), 0);
214// set_timeout (recv/send deadline) + shutdown (graceful SHUT_RDWR) on a live connection.
215// A loopback pair so both run on a real connected socket; shutdown wakes the peer's recv.
216TEST(CheatahSocket, TimeoutThenShutdown) {
217 const long long lfd = sk::tcp_listen("127.0.0.1", 0, 1);
218 ASSERT_GE(lfd, 0) << sk::last_error();
219 const long long port = sk::local_port(lfd);
220 ASSERT_GT(port, 0);
221 std::thread client([&] {
222 const long long c = sk::tcp_connect("127.0.0.1", port);
223 if (c >= 0) { sk::recv(c, 1); sk::close(c); }
224 });
225 const long long conn = sk::accept(lfd);
226 ASSERT_GE(conn, 0) << sk::last_error();
227 EXPECT_EQ(sk::set_timeout(conn, 500), 0); // positive timeout -> SO_RCVTIMEO/SO_SNDTIMEO
228 EXPECT_EQ(sk::shutdown(conn), 0); // graceful SHUT_RDWR
229 sk::close(conn);
230 client.join();
231 sk::close(lfd);
234// ---- owning RAII guards (Conn / Listener): the leak-proof, `with`-friendly API ----
236// A default-constructed Conn owns nothing: closed, fd == -1, and close() reports -1.
237TEST(CheatahSocket, ConnDefaultIsClosed) {
238 sk::Conn c;
239 EXPECT_FALSE(c.is_open());
240 EXPECT_EQ(c.fd(), -1);
241 EXPECT_EQ(c.close(), -1); // nothing to close
242 // A failed open() also yields a closed Conn (no fd leaked on the error path).
243 sk::Listener probe = sk::serve("127.0.0.1", 0, 1);
244 const long long dead = probe.local_port();
245 probe.close();
246 sk::Conn bad = sk::open("127.0.0.1", dead);
247 EXPECT_FALSE(bad.is_open());
250// Likewise for a default-constructed Listener.
251TEST(CheatahSocket, ListenerDefaultIsClosed) {
252 sk::Listener l;
253 EXPECT_FALSE(l.is_open());
254 EXPECT_EQ(l.fd(), -1);
255 EXPECT_EQ(l.close(), -1);
258// Explicit close() releases the fd and is idempotent; the destructor then does nothing.
259TEST(CheatahSocket, ConnGuardClosesOnScopeExit) {
260 sk::Listener server = sk::serve("127.0.0.1", 0, 1);
261 ASSERT_TRUE(server.is_open()) << sk::last_error();
262 sk::Conn c = sk::open("127.0.0.1", server.local_port());
263 ASSERT_TRUE(c.is_open()) << sk::last_error();
264 EXPECT_GE(c.fd(), 0);
265 EXPECT_EQ(c.close(), 0); // explicit close
266 EXPECT_FALSE(c.is_open());
267 EXPECT_EQ(c.close(), -1); // idempotent
268 // `server` is closed by its destructor here — Valgrind confirms no fd leak.
271// Move transfers fd ownership; the moved-from guard is left closed (never double-closed).
272TEST(CheatahSocket, ConnMoveTransfersOwnership) {
273 sk::Listener server = sk::serve("127.0.0.1", 0, 2);
274 ASSERT_TRUE(server.is_open());
275 sk::Conn a = sk::open("127.0.0.1", server.local_port());
276 ASSERT_TRUE(a.is_open());
277 const long long fd = a.fd();
278 sk::Conn b(std::move(a)); // move-construct
279 EXPECT_FALSE(a.is_open());
280 EXPECT_EQ(b.fd(), fd);
281 // Move-assign onto an already-open guard closes the overwritten fd first.
282 sk::Conn d = sk::open("127.0.0.1", server.local_port());
283 ASSERT_TRUE(d.is_open());
284 d = std::move(b);
285 EXPECT_FALSE(b.is_open());
286 EXPECT_EQ(d.fd(), fd);
287 EXPECT_EQ(d.close(), 0);
290// Full loopback through the guards: serve()/open()/accept() + Conn send/sendall/recv/
291// set_timeout/local_port/shutdown/close — the whole owning API on a live connection.
292TEST(CheatahSocket, ConnLoopback) {
293 sk::Listener server = sk::serve("127.0.0.1", 0, 4);
294 ASSERT_TRUE(server.is_open()) << sk::last_error();
295 EXPECT_GE(server.fd(), 0);
296 const long long port = server.local_port();
297 ASSERT_GT(port, 0);
299 std::string client_reply;
300 std::thread client([&] {
301 sk::Conn c = sk::open("127.0.0.1", port);
302 ASSERT_TRUE(c.is_open());
303 EXPECT_EQ(c.set_timeout(1000), 0);
304 EXPECT_GT(c.local_port(), 0);
305 EXPECT_GT(c.send("pi"), 0); // single send()
306 EXPECT_EQ(c.sendall("ng"), 0); // looped sendall()
307 client_reply = c.recv(64);
308 // c is closed by its destructor at thread-scope exit.
309 });
311 sk::Conn conn = server.accept();
312 ASSERT_TRUE(conn.is_open()) << sk::last_error();
313 std::string got;
314 while (got.size() < 4) {
315 const std::string chunk = conn.recv(64);
316 if (chunk.empty()) break;
317 got += chunk;
318 }
319 EXPECT_EQ(got, "ping");
320 EXPECT_EQ(conn.sendall("pong"), 0);
321 EXPECT_EQ(conn.shutdown(), 0); // graceful half-close
322 EXPECT_EQ(conn.close(), 0);
324 client.join();
325 EXPECT_EQ(client_reply, "pong");
326 // `server` is closed by its destructor.
329// Listener move semantics + idempotent close.
330TEST(CheatahSocket, ListenerLoopback) {
331 sk::Listener a = sk::serve("127.0.0.1", 0, 1);
332 ASSERT_TRUE(a.is_open()) << sk::last_error();
333 const long long port = a.local_port();
334 ASSERT_GT(port, 0);
335 sk::Listener b(std::move(a)); // move-construct
336 EXPECT_FALSE(a.is_open());
337 EXPECT_EQ(b.local_port(), port);
338 sk::Listener c;
339 c = std::move(b); // move-assign onto a closed listener
340 EXPECT_FALSE(b.is_open());
341 EXPECT_GE(c.fd(), 0);
342 EXPECT_EQ(c.close(), 0);
343 EXPECT_EQ(c.close(), -1); // idempotent