From 3457ff08cedc0b4f47de7e11e061e1a509d310d6 Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 1 Nov 2026 01:45:31 +0100 Subject: [PATCH] test(listener): cover ListenerQUIC::Port() ShouldReportListenerPortQUIC: a listener on port 0 reports a nonzero port and a client reaches it there; a second listener on port 0 gets a different port; a fixed port reads back as itself; the port is still there after Stop() and moves with the listener. It binds no fixed port of its own: the fixed-port check reuses the ephemeral port a listener just let go of. Co-Authored-By: Claude Opus 5.5 --- project.cpp | 1 + tests/ShouldReportListenerPortQUIC/main.cpp | 107 ++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 tests/ShouldReportListenerPortQUIC/main.cpp diff --git a/project.cpp b/project.cpp index 76a3c66..5b978dd 100644 --- a/project.cpp +++ b/project.cpp @@ -203,6 +203,7 @@ extern "C" Configuration CrafterBuildProject(std::span a cfg.AddTest("ShouldSendRecieveQUICDatagram").Dependencies({ &cfg }); cfg.AddTest("ShouldSendRecieveQUICStream").Dependencies({ &cfg }); cfg.AddTest("ShouldReportClosedQUICStream").Dependencies({ &cfg }); + cfg.AddTest("ShouldReportListenerPortQUIC").Dependencies({ &cfg }); cfg.AddTest("ShouldSurviveConnectionChurn").Dependencies({ &cfg }); } else { // Only a --no-quic build can show that msquic stayed out. diff --git a/tests/ShouldReportListenerPortQUIC/main.cpp b/tests/ShouldReportListenerPortQUIC/main.cpp new file mode 100644 index 0000000..2a0a481 --- /dev/null +++ b/tests/ShouldReportListenerPortQUIC/main.cpp @@ -0,0 +1,107 @@ +//SPDX-License-Identifier: LGPL-3.0-only +//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® + +// ListenerQUIC::Port() says which UDP port the listener is bound to, so port +// 0 (let msquic pick a free one) is usable. Before, nothing read it back: a +// server had to pick a fixed port and hope nothing else held it, and this +// suite hands every QUIC test a port of its own by hand. +// +// Also: two listeners on port 0 get different ports, a fixed port reads back +// as itself, and the port is still there after Stop() and gone from a +// moved-from listener. + +#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; +} + +std::string Text(const std::vector& bytes) { + return std::string(bytes.begin(), bytes.end()); +} + +} // namespace + +int main() { + ThreadPool::Start(); + + // A connect that never completes would otherwise sit out the handshake + // timeout and the runner's 60 s; fail well before that instead. + std::thread([] { + std::this_thread::sleep_for(std::chrono::seconds(30)); + std::println("FAIL timed out"); + std::fflush(stdout); + std::_Exit(1); + }).detach(); + + const std::string alpn = "crafter/test-listener-port"; + QUICServerCredentials serverCreds; + serverCreds.selfSigned = true; + QUICClientCredentials clientCreds; + clientCreds.insecureNoServerValidation = true; + + try { + // ── Port 0: msquic picks, Port() says what it picked ───────────────── + ListenerQUIC listener(0, alpn, serverCreds, [](ClientQUIC* peer) { + peer->OnStream([](QUICStream stream) { + try { + std::vector got = stream.RecieveUntilCloseSync(); + stream.SendSync(got.data(), static_cast(got.size()), true); + } catch (...) {} + }); + }); + listener.ListenAsyncAsync(); + const std::uint16_t port = listener.Port(); + Check(port != 0, std::format("a listener on port 0 reports the port it got ({})", port)); + + { + ClientQUIC client("localhost", port, alpn, clientCreds); + QUICStream stream = client.OpenStream(); + stream.SendSync("ping", 4, true); + const std::string echoed = Text(stream.RecieveUntilCloseSync()); + Check(echoed == "ping", std::format("a client reaches it at that port ({})", echoed)); + } + + // ── Another listener on port 0 gets another port ───────────────────── + std::uint16_t freed = 0; + { + ListenerQUIC other(0, alpn, serverCreds, [](ClientQUIC* peer) { delete peer; }); + freed = other.Port(); + Check(freed != 0 && freed != port, + std::format("a second listener on port 0 gets a port of its own ({} and {})", port, freed)); + + ListenerQUIC moved(std::move(other)); + Check(moved.Port() == freed && other.Port() == 0, + "moving a listener moves its port; the moved-from one reports 0"); + } + + // ── A fixed port reads back as itself ──────────────────────────────── + // The port the listener above let go of: free again, and not one any + // other test in the suite binds. + { + ListenerQUIC fixed(freed, alpn, serverCreds, [](ClientQUIC* peer) { delete peer; }); + Check(fixed.Port() == freed, std::format("a listener on a fixed port reports that port ({})", fixed.Port())); + } + + listener.Stop(); + Check(listener.Port() == port, "Port() still answers after Stop()"); + } catch (const std::exception& e) { + Check(false, std::format("threw: {}", e.what())); + } + + std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed"); + // As the other QUIC tests: skip the static teardown, which waits for the + // accepted peer's idle timeout. _Exit doesn't flush. + std::fflush(stdout); + std::_Exit(failures == 0 ? 0 : 1); +}