//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // ClientTCP's receive calls return exactly the bytes that arrived: // - RecieveSync(bufferSize) returned the whole buffer when fewer bytes had // arrived, so the caller read garbage after them. // - RecieveUntilCloseSync kept the unused end of its first buffer, and then // cut everything it had read down to the size of the last read. // - A signal arriving while a receive waited made it throw "Interrupted // system call", where the receive should simply wait on. // - RecieveSync(bufferSize, buffer) returned recv's -1 or 0 where the other // receives throw: it now returns the count and throws the same way. A // receive of no bytes returns at once: recv's 0 for it isn't a close. // - The *Async receives let a closed peer's exception escape the thread // pool's task, which ended the whole program; it now goes to onError, // and without an onError it's dropped. // // Everything runs over loopback. #include #include #include #include #include #include #include import Crafter.Network; import Crafter.Thread; 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 connected pair: a ClientTCP and the socket the listener accepted for it. struct Connection { int listener = -1; int peer = -1; std::optional client; Connection() { listener = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in address {}; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); bind(listener, reinterpret_cast(&address), sizeof(address)); listen(listener, 1); socklen_t length = sizeof(address); getsockname(listener, reinterpret_cast(&address), &length); client.emplace("127.0.0.1", ntohs(address.sin_port)); peer = accept(listener, nullptr, nullptr); } ~Connection() { if (peer != -1) close(peer); close(listener); } void Send(std::string_view bytes) const { send(peer, bytes.data(), bytes.size(), 0); } void ClosePeer() { close(peer); peer = -1; } }; std::string_view Text(const std::vector& bytes) { return std::string_view(bytes.data(), bytes.size()); } // What a receive threw, by kind. std::string Kind(std::exception_ptr error) { try { std::rethrow_exception(error); } catch (const SocketClosedException&) { return "closed"; } catch (const SocketTimeoutException&) { return "timeout"; } catch (const std::exception& e) { return std::string("other: ") + e.what(); } } // An async receive's outcome. Shared with the callbacks, so it outlives // whichever thread sets it. struct Outcome { std::mutex mutex; std::condition_variable done; std::optional value; void Set(std::string what) { { std::lock_guard lock(mutex); value = std::move(what); } done.notify_all(); } std::string Wait() { std::unique_lock lock(mutex); done.wait_for(lock, 2s, [&] { return value.has_value(); }); return value.value_or("nothing within 2 s"); } }; void IgnoreSignal(int) {} } // namespace int main() { // ── A short read returns only what arrived ─────────────────────────────── { Connection connection; connection.Send("hello"); std::this_thread::sleep_for(20ms); const std::vector received = connection.client->RecieveSync(64); Check(Text(received) == "hello", std::format("RecieveSync(64) returns the 5 bytes that arrived (got {})", received.size())); } // ── Until close: every chunk, in order, nothing more ───────────────────── { Connection connection; std::thread sender([&] { connection.Send("abc"); std::this_thread::sleep_for(30ms); connection.Send("defgh"); std::this_thread::sleep_for(30ms); connection.Send("ij"); std::this_thread::sleep_for(30ms); connection.ClosePeer(); }); const std::vector received = connection.client->RecieveUntilCloseSync(); sender.join(); Check(Text(received) == "abcdefghij", std::format("RecieveUntilCloseSync returns the three chunks it was sent ({} bytes)", received.size())); } // ── A signal doesn't break a waiting receive ───────────────────────────── { // No SA_RESTART, so the signal makes recv return EINTR. struct sigaction action {}; action.sa_handler = IgnoreSignal; sigemptyset(&action.sa_mask); sigaction(SIGUSR1, &action, nullptr); Connection connection; std::string outcome; std::thread reader([&] { try { outcome = Text(connection.client->RecieveUntilFullSync(4)); } catch (const std::exception& e) { outcome = std::string("threw: ") + e.what(); } }); std::this_thread::sleep_for(50ms); pthread_kill(reader.native_handle(), SIGUSR1); std::this_thread::sleep_for(50ms); connection.Send("data"); reader.join(); Check(outcome == "data", std::format("a receive interrupted by a signal waits on for its data ({})", outcome)); } // ── Into a buffer: the count, and the same exceptions ──────────────────── { Connection connection; connection.Send("hello"); std::this_thread::sleep_for(20ms); std::array buffer {}; const std::uint32_t got = connection.client->RecieveSync(buffer.size(), buffer.data()); Check(got == 5 && std::string_view(buffer.data(), got) == "hello", std::format("RecieveSync(64, buffer) returns the 5 bytes that arrived ({})", got)); connection.ClosePeer(); std::string closed; try { (void)connection.client->RecieveSync(buffer.size(), buffer.data()); } catch (...) { closed = Kind(std::current_exception()); } Check(closed == "closed", std::format("and throws SocketClosedException once the peer has closed ({})", closed.empty() ? "returned" : closed)); } { Connection connection; std::array buffer {}; std::string outcome = "returned"; try { const bool empty = connection.client->RecieveSync(0).empty() && connection.client->RecieveSync(0, buffer.data()) == 0; if (!empty) outcome = "bytes"; } catch (...) { outcome = Kind(std::current_exception()); } Check(outcome == "returned", std::format("a receive of no bytes returns nothing on a live connection ({})", outcome)); } { Connection connection; connection.client->SetReceiveTimeout(50ms); std::array buffer {}; std::string timedOut; try { (void)connection.client->RecieveSync(buffer.size(), buffer.data()); } catch (...) { timedOut = Kind(std::current_exception()); } Check(timedOut == "timeout", std::format("and SocketTimeoutException when nothing comes in time ({})", timedOut.empty() ? "returned" : timedOut)); } // ── Async: data to the callback, failures to onError ───────────────────── ThreadPool::Start(3); { Connection connection; connection.Send("hi"); auto outcome = std::make_shared(); connection.client->RecieveAsync(64, [outcome](std::vector data) { outcome->Set(std::string(Text(data))); }, [outcome](std::exception_ptr error) { outcome->Set(Kind(error)); }); const std::string got = outcome->Wait(); Check(got == "hi", std::format("RecieveAsync hands the data to its callback ({})", got)); } { Connection connection; connection.ClosePeer(); auto outcome = std::make_shared(); connection.client->RecieveAsync(64, [outcome](std::vector) { outcome->Set("data"); }, [outcome](std::exception_ptr error) { outcome->Set(Kind(error)); }); const std::string got = outcome->Wait(); Check(got == "closed", std::format("and a closed peer to onError, the program carrying on ({})", got)); } { Connection connection; connection.Send("ab"); connection.ClosePeer(); auto outcome = std::make_shared(); connection.client->RecieveUntilFullAsync(4, [outcome](std::vector) { outcome->Set("data"); }, [outcome](std::exception_ptr error) { outcome->Set(Kind(error)); }); const std::string got = outcome->Wait(); Check(got == "closed", std::format("RecieveUntilFullAsync reports a peer that closes early to onError ({})", got)); } { Connection connection; connection.Send("xyz"); std::array buffer {}; auto outcome = std::make_shared(); connection.client->RecieveAsync(buffer.size(), [outcome, &buffer](std::uint32_t count) { outcome->Set(std::string(buffer.data(), count)); }, buffer.data(), [outcome](std::exception_ptr error) { outcome->Set(Kind(error)); }); const std::string got = outcome->Wait(); Check(got == "xyz", std::format("the buffer RecieveAsync hands its callback the count ({})", got)); } { Connection connection; connection.ClosePeer(); auto outcome = std::make_shared(); connection.client->RecieveUntilCloseAsync([outcome](std::vector) { outcome->Set("data"); }, [outcome](std::exception_ptr error) { outcome->Set(Kind(error)); }); const std::string got = outcome->Wait(); Check(got == "closed", std::format("RecieveUntilCloseAsync on a peer that sent nothing goes to onError ({})", got)); } { Connection connection; connection.ClosePeer(); connection.client->RecieveAsync(64, [](std::vector) {}, {}); // Calling the empty onError ended the program from the pool's task. std::this_thread::sleep_for(300ms); Check(true, "without an onError, a closed peer's exception is dropped, the program carrying on"); } ThreadPool::Stop(); std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed"); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }