Crafter.Network/tests/ShouldConfigureTCP/main.cpp

150 lines
6 KiB
C++
Raw Permalink Normal View History

feat: TCP-only builds; TCP options and timeouts; receives that report and keep what arrived - --no-quic leaves QUIC, HTTP/3 and WebTransport out of the build, and with them msquic: a consumer that only needs TCP builds no msquic. The flag isn't passed on to dependencies. - --local takes Crafter.Thread from the sibling checkout, as the other Crafter projects do. - ClientTCP gains SetNoDelay, SetReceiveTimeout, a connect timeout and SocketTimeoutException. - TCP receives return the bytes that arrived: - RecieveSync(n) returned the whole buffer when fewer had come; - RecieveUntilCloseSync truncated what it had read; - a signal (EINTR) during a receive threw where it should wait on; - RecieveSync(size, buffer) returns the count and throws like the others; - a receive of no bytes returns at once: recv's 0 for it isn't a close. - Every TCP *Async receive takes an onError(std::exception_ptr): an exception escaping a ThreadPool task ended the program. An empty onError drops the error. - QUIC streams: - async receives deliver an empty chunk on close, as the browser build does; - RecieveUntilFullAsync gives what arrived on a stream that closes early; - RecieveUntilFullSync takes all it asked for or nothing, under one hold of the lock, so what did arrive stays for the next receive and no other receive can take bytes from among it; - SendAsync's callback hears whether the send went out (bool sent): native, browser, the JS bridge and the example. - msquic is pinned to v2.6.3 and built against the system OpenSSL (QUIC_TLS_LIB=openssl, 3.5 or newer). Its bundled quictls read the system's openssl.cnf, and the Flatpak runtime's (a PKCS#11 engine, with config_diagnostics on) made it fail to initialise. The README says to use --no-quic where the distro's OpenSSL is older. - Tests now register on any Linux triple: they never ran under the Flatpak's x86_64-unknown-linux-gnu. New: ShouldBuildWithoutQUIC, ShouldConfigureTCP, ShouldRecieveTCP, ShouldReportClosedQUICStream. Breaking: SendAsync's callback takes a bool, the TCP *Async receives take an onError, and RecieveSync(size, buffer) returns the count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 19:07:29 +02:00
//SPDX-License-Identifier: LGPL-3.0-only
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// ClientTCP's socket options:
// - SetNoDelay turns Nagle's algorithm off, so a small message goes out at
// once instead of waiting for the ACK of the one before it, which a client
// streaming small frames needs.
// - A receive timeout makes a receive that waits too long throw
// SocketTimeoutException.
// - A connect timeout does the same for a connect nobody answers, where
// connect() would otherwise sit through the kernel's SYN retries, about
// two minutes.
// Before, a ClientTCP had none of these, so a stalled peer blocked the
// reading thread for good.
//
// Everything runs over loopback. The unanswered connect goes to a listener
// whose accept queue is full: Linux drops the SYNs of further connects to it.
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cstdlib>
import Crafter.Network;
import std;
using namespace Crafter;
using namespace std::chrono_literals;
namespace {
int failures = 0;
void Check(bool ok, std::string_view what) {
std::println("{} {}", ok ? "PASS" : "FAIL", what);
if (!ok) ++failures;
}
// A socket listening on 127.0.0.1, on a port the kernel picks.
struct LoopbackListener {
int fd = -1;
std::uint16_t port = 0;
explicit LoopbackListener(int backlog) {
fd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in address {};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
bind(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address));
listen(fd, backlog);
socklen_t length = sizeof(address);
getsockname(fd, reinterpret_cast<sockaddr*>(&address), &length);
port = ntohs(address.sin_port);
}
~LoopbackListener() { close(fd); }
};
long long Milliseconds(std::chrono::steady_clock::duration duration) {
return std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
}
} // namespace
int main() {
// ── SetNoDelay ───────────────────────────────────────────────────────────
{
LoopbackListener listener(4);
ClientTCP client("127.0.0.1", listener.port);
auto noDelay = [&] {
int value = 0;
socklen_t size = sizeof(value);
getsockopt(client.socketid, IPPROTO_TCP, TCP_NODELAY, &value, &size);
return value != 0;
};
Check(!noDelay(), "a new connection uses Nagle's algorithm");
client.SetNoDelay(true);
Check(noDelay(), "SetNoDelay(true) turns it off");
client.SetNoDelay(false);
Check(!noDelay(), "SetNoDelay(false) turns it back on");
}
// ── Receive timeout ──────────────────────────────────────────────────────
{
LoopbackListener listener(4);
ClientTCP client("127.0.0.1", listener.port);
const int peer = accept(listener.fd, nullptr, nullptr);
client.SetReceiveTimeout(150ms);
const auto start = std::chrono::steady_clock::now();
bool timedOut = false;
try {
client.RecieveUntilFullSync(16);
} catch (const SocketTimeoutException&) {
timedOut = true;
}
const auto waited = std::chrono::steady_clock::now() - start;
Check(timedOut, "a receive from a peer that sends nothing throws SocketTimeoutException");
Check(waited >= 140ms && waited < 1s, std::format("after about the timeout ({} ms)", Milliseconds(waited)));
send(peer, "hello", 5, 0);
const std::vector<char> received = client.RecieveUntilFullSync(5);
Check(std::string_view(received.data(), received.size()) == "hello",
"the connection still receives what arrives after a timeout");
close(peer);
}
// ── Connect timeout ──────────────────────────────────────────────────────
{
// Connects queue up until the accept queue is full, which with a
// backlog of 0 is after one or two; the next one goes unanswered.
LoopbackListener full(0);
std::vector<ClientTCP> queued;
bool timedOut = false;
std::chrono::steady_clock::duration waited {};
for (int i = 0; i < 8 && !timedOut; ++i) {
const auto start = std::chrono::steady_clock::now();
try {
queued.emplace_back("127.0.0.1", full.port, 200ms);
} catch (const SocketTimeoutException&) {
timedOut = true;
waited = std::chrono::steady_clock::now() - start;
}
}
Check(timedOut, std::format("a connect nobody answers throws SocketTimeoutException (after {} queued)", queued.size()));
Check(waited >= 190ms && waited < 1s, std::format("after about the timeout ({} ms)", Milliseconds(waited)));
}
// ── A refused connect still fails at once ────────────────────────────────
{
std::uint16_t closedPort = 0;
{
LoopbackListener gone(1);
closedPort = gone.port;
}
const auto start = std::chrono::steady_clock::now();
bool refused = false;
try {
ClientTCP client("127.0.0.1", closedPort, 2s);
} catch (const SocketTimeoutException&) {
} catch (const std::runtime_error&) {
refused = true;
}
Check(refused && std::chrono::steady_clock::now() - start < 1s,
"a connect to a closed port fails at once, not after the timeout");
}
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
}