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 <cstddef>
6#include <chrono>
7#include <string>
8#include <thread>
9#include <utility>
11#if !defined(_WIN32)
12#include <netinet/in.h>
13#include <netinet/tcp.h>
14#include <sys/socket.h>
15#endif
17#include <gtest/gtest.h>
19namespace sk = cheatah::socket;
21// A real loopback round-trip: a listener accepts one client, reads its request,
22// and replies; a client thread connects, sends, and reads the reply. Exercises
23// tcp_listen / local_port / accept / recv / sendall / tcp_connect / send / close.
24TEST(CheatahSocket, Loopback) {
25 const long long lfd = sk::tcp_listen("127.0.0.1", 0, 4);
26 ASSERT_GE(lfd, 0) << sk::last_error();
27 const long long port = sk::local_port(lfd);
28 ASSERT_GT(port, 0);
30 std::string client_reply;
31 std::thread client([&] {
32 const long long cfd = sk::tcp_connect("127.0.0.1", port);
33 ASSERT_GE(cfd, 0);
34 ASSERT_EQ(sk::sendall(cfd, "ping"), 0);
35 client_reply = sk::recv(cfd, 64);
36 sk::close(cfd);
37 });
39 const long long conn = sk::accept(lfd);
40 ASSERT_GE(conn, 0) << sk::last_error();
41 EXPECT_EQ(sk::recv(conn, 64), "ping");
42 EXPECT_EQ(sk::sendall(conn, "pong"), 0);
43 sk::close(conn);
45 client.join();
46 EXPECT_EQ(client_reply, "pong");
47 sk::close(lfd);
50// A larger, binary-ish payload to drive send()/sendall() through multiple writes
51// and confirm byte-exactness (including an embedded NUL).
52TEST(CheatahSocket, Sendall) {
53 const long long lfd = sk::tcp_listen("127.0.0.1", 0, 1);
54 ASSERT_GE(lfd, 0);
55 const long long port = sk::local_port(lfd);
57 std::string payload(8000, 'x');
58 payload[100] = '\0';
59 payload[7999] = 'Z';
61 std::thread client([&] {
62 const long long cfd = sk::tcp_connect("127.0.0.1", port);
63 ASSERT_GE(cfd, 0);
64 EXPECT_GT(sk::send(cfd, "hi"), 0); // single send()
65 EXPECT_EQ(sk::sendall(cfd, payload), 0);
66 sk::close(cfd);
67 });
69 const long long conn = sk::accept(lfd);
70 ASSERT_GE(conn, 0);
71 std::string got;
72 while (got.size() < payload.size() + 2) {
73 const std::string chunk = sk::recv(conn, 4096);
74 if (chunk.empty()) break;
75 got += chunk;
76 }
77 EXPECT_EQ(got, "hi" + payload);
78 sk::close(conn);
79 client.join();
80 sk::close(lfd);
83#if !defined(_WIN32)
84// THE "it's the default for every user" guarantee: every connected stream socket — client AND
85// accepted server side — comes tuned for throughput (TCP_NODELAY on, an enlarged SO_RCVBUF)
86// WITHOUT any caller opt-in. This is the regression guard that a future refactor can't silently
87// drop the tuning that turns a 0.2 MB/s crawl into a full-speed download.
88TEST(CheatahSocket, ConnectedSocketsAreTunedByDefault) {
89 const long long lfd = sk::tcp_listen("127.0.0.1", 0, 1);
90 ASSERT_GE(lfd, 0) << sk::last_error();
91 const long long port = sk::local_port(lfd);
93 long long cfd = -1;
94 std::thread client([&] { cfd = sk::tcp_connect("127.0.0.1", port); });
95 const long long conn = sk::accept(lfd);
96 client.join();
97 ASSERT_GE(cfd, 0) << sk::last_error();
98 ASSERT_GE(conn, 0) << sk::last_error();
100 const auto nodelay = [](long long fd) {
101 int v = 0;
102 socklen_t len = sizeof(v);
103 EXPECT_EQ(::getsockopt(static_cast<int>(fd), IPPROTO_TCP, TCP_NODELAY, &v, &len), 0);
104 return v;
105 };
106 const auto rcvbuf = [](long long fd) {
107 int v = 0;
108 socklen_t len = sizeof(v);
109 EXPECT_EQ(::getsockopt(static_cast<int>(fd), SOL_SOCKET, SO_RCVBUF, &v, &len), 0);
110 return v; // Linux reports 2× the requested value
111 };
112 // Nagle is OFF on both ends.
113 EXPECT_NE(nodelay(cfd), 0) << "client socket left Nagle on";
114 EXPECT_NE(nodelay(conn), 0) << "accepted socket left Nagle on";
115 // The receive buffer is well above the kernel's stock default (131072 here) — proof the
116 // window was opened. Floor chosen below the requested 4 MiB to tolerate rmem_max clamping.
117 EXPECT_GE(rcvbuf(cfd), 512 * 1024) << "client SO_RCVBUF not enlarged";
118 EXPECT_GE(rcvbuf(conn), 512 * 1024) << "accepted SO_RCVBUF not enlarged";
120 sk::close(cfd);
121 sk::close(conn);
122 sk::close(lfd);
125// A multi-megabyte loopback transfer read back byte-for-byte, and fast — the socket-layer
126// throughput regression guard. If a change reverted the tuning or crippled recv, a several-MB
127// loopback transfer would still be correct but this asserts it also stays quick.
128TEST(CheatahSocket, BulkTransferIsCorrectAndPrompt) {
129 const long long lfd = sk::tcp_listen("127.0.0.1", 0, 1);
130 ASSERT_GE(lfd, 0);
131 const long long port = sk::local_port(lfd);
133 // 8 MiB with a position-dependent pattern so any misorder/truncation is caught.
134 std::string payload(std::size_t{8} * 1024 * 1024, '\0');
135 for (std::size_t i = 0; i < payload.size(); ++i) {
136 payload[i] = static_cast<char>((i * 1103515245u + 12345u) >> 16);
137 }
139 std::thread server([&] {
140 const long long conn = sk::accept(lfd);
141 ASSERT_GE(conn, 0);
142 EXPECT_EQ(sk::sendall(conn, payload), 0);
143 sk::close(conn);
144 });
146 const auto t0 = std::chrono::steady_clock::now();
147 const long long cfd = sk::tcp_connect("127.0.0.1", port);
148 ASSERT_GE(cfd, 0);
149 std::string got;
150 got.reserve(payload.size());
151 while (got.size() < payload.size()) {
152 const std::string chunk = sk::recv(cfd, 65536);
153 if (chunk.empty()) break;
154 got += chunk;
155 }
156 const double secs = std::chrono::duration<double>(std::chrono::steady_clock::now() - t0).count();
157 sk::close(cfd);
158 server.join();
159 sk::close(lfd);
161 ASSERT_EQ(got.size(), payload.size());
162 EXPECT_EQ(got, payload);
163 // Loopback moves GB/s; 8 MiB in >5 s would mean the recv path regressed hard. Generous so CI
164 // load never flakes it.
165 EXPECT_LT(secs, 5.0) << "8 MiB loopback took " << secs << "s — recv path regressed";
167#endif // !_WIN32
169// The low-level BSD path: socket -> set_reuseaddr -> bind -> listen -> local_port.
170TEST(CheatahSocket, ListenLowLevel) {
171 const long long fd = sk::socket();
172 ASSERT_GE(fd, 0);
173 EXPECT_EQ(sk::set_reuseaddr(fd), 0);
174 EXPECT_EQ(sk::bind(fd, "127.0.0.1", 0), 0);
175 EXPECT_EQ(sk::listen(fd, 1), 0);
176 EXPECT_GT(sk::local_port(fd), 0);
177 EXPECT_EQ(sk::close(fd), 0);
180// Connecting to a port nobody is listening on fails, and last_error() is set.
181// A datagram round trip on loopback: a bound receiver, an unbound sender, the sender's address
182// reported back, a receive window that expires empty, and a packet larger than the buffer truncated
183// — every property the datagram contract promises, none of the stream's.
184TEST(CheatahSocket, UdpLoopback) {
185 const long long rx = sk::udp_socket();
186 ASSERT_GE(rx, 0) << sk::last_error();
187 ASSERT_EQ(sk::bind(rx, "127.0.0.1", 0), 0) << sk::last_error();
188 const long long port = sk::local_port(rx);
189 ASSERT_GT(port, 0);
190 ASSERT_EQ(sk::set_timeout(rx, 500), 0);
191 const long long tx = sk::udp_socket();
192 ASSERT_GE(tx, 0);
193 EXPECT_EQ(sk::sendto(tx, "127.0.0.1", port, "presence 1"), 10);
194 std::string from;
195 long long from_port = 0;
196 EXPECT_EQ(sk::recvfrom(rx, 64, from, from_port), "presence 1");
197 EXPECT_EQ(from, "127.0.0.1");
198 EXPECT_GT(from_port, 0);
199 // Nothing sent: the window passes and the receive is empty, not an error.
200 EXPECT_EQ(sk::recvfrom(rx, 64, from, from_port), "");
201 EXPECT_EQ(from_port, 0);
202 // A packet larger than the buffer is truncated to it — the datagram contract, stated in the doc.
203 EXPECT_EQ(sk::sendto(tx, "127.0.0.1", port, std::string(100, 'x')), 100);
204 EXPECT_EQ(sk::recvfrom(rx, 16, from, from_port).size(), 16u);
205 EXPECT_EQ(sk::close(tx), 0);
206 EXPECT_EQ(sk::close(rx), 0);
209TEST(CheatahSocket, ConnectRefused) {
210 // Bind+listen to grab a free port, then close it so the port is free again.
211 const long long probe = sk::tcp_listen("127.0.0.1", 0, 1);
212 ASSERT_GE(probe, 0);
213 const long long dead_port = sk::local_port(probe);
214 sk::close(probe);
216 EXPECT_LT(sk::tcp_connect("127.0.0.1", dead_port), 0);
217 EXPECT_FALSE(sk::last_error().empty());
220// Name resolution failure is reported as -1, not a crash.
221TEST(CheatahSocket, ResolveFailure) {
222 const long long fd = sk::socket();
223 ASSERT_GE(fd, 0);
224 EXPECT_LT(sk::bind(fd, "no.such.host.invalid.", 0), 0);
225 EXPECT_LT(sk::tcp_connect("no.such.host.invalid.", 80), 0);
226 EXPECT_LT(sk::tcp_listen("no.such.host.invalid.", 0, 1), 0); // exercises tcp_listen's error path
227 sk::close(fd);
230// Operations on a bad fd return errors (and recv returns ""), never crash.
231TEST(CheatahSocket, BadFd) {
232 EXPECT_EQ(sk::recv(-1, 64), "");
233 EXPECT_EQ(sk::recv(5, 0), ""); // non-positive bufsize
234 EXPECT_LT(sk::send(-1, "x"), 0);
235 EXPECT_LT(sk::sendall(-1, "x"), 0);
236 EXPECT_LT(sk::listen(-1, 1), 0);
237 EXPECT_LT(sk::accept(-1), 0);
238 EXPECT_LT(sk::local_port(-1), 0);
239 EXPECT_LT(sk::connect(-1, "127.0.0.1", 80), 0);
240 EXPECT_LT(sk::close(-1), 0);
243// set_timeout (recv/send deadline) + shutdown (graceful SHUT_RDWR) on a live connection.
244// A loopback pair so both run on a real connected socket; shutdown wakes the peer's recv.
245TEST(CheatahSocket, TimeoutThenShutdown) {
246 const long long lfd = sk::tcp_listen("127.0.0.1", 0, 1);
247 ASSERT_GE(lfd, 0) << sk::last_error();
248 const long long port = sk::local_port(lfd);
249 ASSERT_GT(port, 0);
250 std::thread client([&] {
251 const long long c = sk::tcp_connect("127.0.0.1", port);
252 if (c >= 0) { sk::recv(c, 1); sk::close(c); }
253 });
254 const long long conn = sk::accept(lfd);
255 ASSERT_GE(conn, 0) << sk::last_error();
256 EXPECT_EQ(sk::set_timeout(conn, 500), 0); // positive timeout -> SO_RCVTIMEO/SO_SNDTIMEO
257 EXPECT_EQ(sk::shutdown(conn), 0); // graceful SHUT_RDWR
258 sk::close(conn);
259 client.join();
260 sk::close(lfd);
263// ---- owning RAII guards (Conn / Listener): the leak-proof, `with`-friendly API ----
265// A default-constructed Conn owns nothing: closed, fd == -1, and close() reports -1.
266TEST(CheatahSocket, ConnDefaultIsClosed) {
267 sk::Conn c;
268 EXPECT_FALSE(c.is_open());
269 EXPECT_EQ(c.fd(), -1);
270 EXPECT_EQ(c.close(), -1); // nothing to close
271 // A failed open() also yields a closed Conn (no fd leaked on the error path).
272 sk::Listener probe = sk::serve("127.0.0.1", 0, 1);
273 const long long dead = probe.local_port();
274 probe.close();
275 sk::Conn bad = sk::open("127.0.0.1", dead);
276 EXPECT_FALSE(bad.is_open());
279// Likewise for a default-constructed Listener.
280TEST(CheatahSocket, ListenerDefaultIsClosed) {
281 sk::Listener l;
282 EXPECT_FALSE(l.is_open());
283 EXPECT_EQ(l.fd(), -1);
284 EXPECT_EQ(l.close(), -1);
287// Explicit close() releases the fd and is idempotent; the destructor then does nothing.
288TEST(CheatahSocket, ConnGuardClosesOnScopeExit) {
289 sk::Listener server = sk::serve("127.0.0.1", 0, 1);
290 ASSERT_TRUE(server.is_open()) << sk::last_error();
291 sk::Conn c = sk::open("127.0.0.1", server.local_port());
292 ASSERT_TRUE(c.is_open()) << sk::last_error();
293 EXPECT_GE(c.fd(), 0);
294 EXPECT_EQ(c.close(), 0); // explicit close
295 EXPECT_FALSE(c.is_open());
296 EXPECT_EQ(c.close(), -1); // idempotent
297 // `server` is closed by its destructor here — Valgrind confirms no fd leak.
300// Move transfers fd ownership; the moved-from guard is left closed (never double-closed).
301TEST(CheatahSocket, ConnMoveTransfersOwnership) {
302 sk::Listener server = sk::serve("127.0.0.1", 0, 2);
303 ASSERT_TRUE(server.is_open());
304 sk::Conn a = sk::open("127.0.0.1", server.local_port());
305 ASSERT_TRUE(a.is_open());
306 const long long fd = a.fd();
307 sk::Conn b(std::move(a)); // move-construct
308 EXPECT_FALSE(a.is_open()); // NOLINT(bugprone-use-after-move,clang-analyzer-cplusplus.Move): moved-from state is the assertion
309 EXPECT_EQ(b.fd(), fd);
310 // Move-assign onto an already-open guard closes the overwritten fd first.
311 sk::Conn d = sk::open("127.0.0.1", server.local_port());
312 ASSERT_TRUE(d.is_open());
313 d = std::move(b);
314 EXPECT_FALSE(b.is_open()); // NOLINT(bugprone-use-after-move,clang-analyzer-cplusplus.Move): moved-from state is the assertion
315 EXPECT_EQ(d.fd(), fd);
316 EXPECT_EQ(d.close(), 0);
319// Full loopback through the guards: serve()/open()/accept() + Conn send/sendall/recv/
320// set_timeout/local_port/shutdown/close — the whole owning API on a live connection.
321TEST(CheatahSocket, ConnLoopback) {
322 sk::Listener server = sk::serve("127.0.0.1", 0, 4);
323 ASSERT_TRUE(server.is_open()) << sk::last_error();
324 EXPECT_GE(server.fd(), 0);
325 const long long port = server.local_port();
326 ASSERT_GT(port, 0);
328 std::string client_reply;
329 std::thread client([&] {
330 sk::Conn c = sk::open("127.0.0.1", port);
331 ASSERT_TRUE(c.is_open());
332 EXPECT_EQ(c.set_timeout(1000), 0);
333 EXPECT_GT(c.local_port(), 0);
334 EXPECT_GT(c.send("pi"), 0); // single send()
335 EXPECT_EQ(c.sendall("ng"), 0); // looped sendall()
336 client_reply = c.recv(64);
337 // c is closed by its destructor at thread-scope exit.
338 });
340 sk::Conn conn = server.accept();
341 ASSERT_TRUE(conn.is_open()) << sk::last_error();
342 std::string got;
343 while (got.size() < 4) {
344 const std::string chunk = conn.recv(64);
345 if (chunk.empty()) break;
346 got += chunk;
347 }
348 EXPECT_EQ(got, "ping");
349 EXPECT_EQ(conn.sendall("pong"), 0);
350 EXPECT_EQ(conn.shutdown(), 0); // graceful half-close
351 EXPECT_EQ(conn.close(), 0);
353 client.join();
354 EXPECT_EQ(client_reply, "pong");
355 // `server` is closed by its destructor.
358// Listener move semantics + idempotent close.
359TEST(CheatahSocket, ListenerLoopback) {
360 sk::Listener a = sk::serve("127.0.0.1", 0, 1);
361 ASSERT_TRUE(a.is_open()) << sk::last_error();
362 const long long port = a.local_port();
363 ASSERT_GT(port, 0);
364 sk::Listener b(std::move(a)); // move-construct
365 EXPECT_FALSE(a.is_open()); // NOLINT(bugprone-use-after-move,clang-analyzer-cplusplus.Move): moved-from state is the assertion
366 EXPECT_EQ(b.local_port(), port);
367 sk::Listener c;
368 c = std::move(b); // move-assign onto a closed listener
369 EXPECT_FALSE(b.is_open()); // NOLINT(bugprone-use-after-move,clang-analyzer-cplusplus.Move): moved-from state is the assertion
370 EXPECT_GE(c.fd(), 0);
371 EXPECT_EQ(c.close(), 0);
372 EXPECT_EQ(c.close(), -1); // idempotent