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 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-11-01 01:45:31 +01:00
commit 3457ff08ce
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
2 changed files with 108 additions and 0 deletions

View file

@ -203,6 +203,7 @@ extern "C" Configuration CrafterBuildProject(std::span<const std::string_view> 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.

View file

@ -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 <cstdio>
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<char>& 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<char> got = stream.RecieveUntilCloseSync();
stream.SendSync(got.data(), static_cast<std::uint32_t>(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);
}