diff --git a/project.cpp b/project.cpp index b30f208..7137192 100644 --- a/project.cpp +++ b/project.cpp @@ -205,6 +205,7 @@ extern "C" Configuration CrafterBuildProject(std::span a cfg.AddTest("ShouldSendRecieveQUICStream").Dependencies({ &cfg }); cfg.AddTest("ShouldReportClosedQUICStream").Dependencies({ &cfg }); cfg.AddTest("ShouldReportListenerPortQUIC").Dependencies({ &cfg }); + cfg.AddTest("ShouldReturnFromSendSyncQUIC").Dependencies({ &cfg }); cfg.AddTest("ShouldShareSelfSignedCertificateQUIC").Dependencies({ &cfg }); cfg.AddTest("ShouldSurviveConnectionChurn").Dependencies({ &cfg }); cfg.AddTest("ShouldTellPeerOfCloseQUIC").Dependencies({ &cfg }); diff --git a/tests/ShouldReturnFromSendSyncQUIC/main.cpp b/tests/ShouldReturnFromSendSyncQUIC/main.cpp new file mode 100644 index 0000000..0bd6174 --- /dev/null +++ b/tests/ShouldReturnFromSendSyncQUIC/main.cpp @@ -0,0 +1,109 @@ +//SPDX-License-Identifier: LGPL-3.0-only +//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// QUICStream::SendSync returns once msquic has taken its buffer, and throws +// only when that send failed. It used to wait on one flag per stream and, +// once woken, throw QUICClosedException whenever the stream had finished +// shutting down meanwhile, though its own send had completed. A peer that +// answers at once and finishes the stream (here: echo, FIN, and drop the +// stream, whose Stop() stops our sending side too) can complete the stream +// within a round trip, which on loopback is shorter than a busy machine can +// take to wake the sending thread: the sender threw for a send the peer had +// already answered. +// +// The race needs the sending thread to be descheduled at the wrong moment, +// so no test hits it every time. The shape that showed it: the first stream +// of a fresh connection, four connections at once, one exchange each; a +// throwaway run of that shape threw once in 3000 exchanges, the echo of the +// failed send arriving intact. Here 2000 such exchanges must all return +// from SendSync, and all be echoed. + +#include + +import Crafter.Network; +import Crafter.Thread; +import std; +using namespace Crafter; + +namespace { + +int failures = 0; + +void Check(bool ok, std::string_view what) { + std::println("{} {}", ok ? "PASS" : "FAIL", what); + if (!ok) ++failures; +} + +} // namespace + +int main() { + ThreadPool::Start(); + std::thread([] { + std::this_thread::sleep_for(std::chrono::seconds(50)); + std::println("FAIL timed out"); + std::fflush(stdout); + std::_Exit(1); + }).detach(); + + const std::string alpn = "crafter/test-send-sync"; + QUICServerCredentials serverCreds; + serverCreds.selfSigned = true; + QUICClientCredentials clientCreds; + clientCreds.insecureNoServerValidation = true; + + constexpr int rounds = 500; + constexpr int together = 4; + std::atomic threw = 0; + std::atomic echoed = 0; + std::atomic other = 0; + try { + // Answer each stream with what arrived and a FIN, then drop it: its + // destructor's Stop() aborts our receive side, which also ends the + // client's sending side. + ListenerQUIC listener("127.0.0.1", 0, alpn, serverCreds, [](ClientQUIC* peer) { + peer->OnStream([](QUICStream stream) { + try { + std::vector got = stream.RecieveSync(); + stream.SendSync(got.data(), static_cast(got.size()), true); + } catch (...) {} + }); + }); + listener.ListenAsyncAsync(); + + for (int round = 0; round < rounds; ++round) { + std::vector clients; + for (int i = 0; i < together; ++i) { + clients.emplace_back([&] { + try { + ClientQUIC client("127.0.0.1", listener.Port(), alpn, clientCreds); + QUICStream stream = client.OpenStream(); + try { + stream.SendSync("ping", 4, false); + } catch (const QUICClosedException&) { + ++threw; + } + std::vector back = stream.RecieveUntilCloseSync(); + if (std::string(back.begin(), back.end()) == "ping") ++echoed; + } catch (const std::exception& e) { + if (++other == 1) std::println(" {}", e.what()); + } + }); + } + for (std::thread& client : clients) client.join(); + } + } catch (const std::exception& e) { + Check(false, std::format("threw: {}", e.what())); + } + constexpr int exchanges = rounds * together; + Check(threw.load() == 0, + std::format("SendSync returned for every send msquic took ({} of {} threw QUICClosedException)", + threw.load(), exchanges)); + Check(echoed.load() == exchanges && other.load() == 0, + std::format("and every echo came back ({} of {}, {} other errors)", echoed.load(), exchanges, other.load())); + + std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed"); + // The accepted peers are left to the process: skip msquic's static + // teardown, as the other QUIC tests do. _Exit doesn't flush. + std::fflush(stdout); + std::_Exit(failures == 0 ? 0 : 1); +}