135 lines
5.4 KiB
C++
135 lines
5.4 KiB
C++
|
|
//SPDX-License-Identifier: LGPL-3.0-only
|
||
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
// A QUIC stream's async calls report a closed stream to their callbacks, the
|
||
|
|
// same way on native as in the browser build, which has no exceptions:
|
||
|
|
// - a receive on a closed stream gets an empty chunk. Natively it used to
|
||
|
|
// let QUICClosedException escape the ThreadPool task, which ended the
|
||
|
|
// program;
|
||
|
|
// - RecieveUntilFullAsync on a stream that closes early gets what arrived;
|
||
|
|
// RecieveUntilFullSync still throws, and leaves those bytes for the next
|
||
|
|
// receive instead of dropping them;
|
||
|
|
// - SendAsync tells its callback whether the stream took the buffer. It
|
||
|
|
// used to call it the same way either way.
|
||
|
|
// A server answers a stream with "abc" and closes its side.
|
||
|
|
|
||
|
|
#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;
|
||
|
|
}
|
||
|
|
|
||
|
|
// A callback's result, shared with it so it outlives whichever thread sets it.
|
||
|
|
template <typename T>
|
||
|
|
struct Outcome {
|
||
|
|
std::mutex mutex;
|
||
|
|
std::condition_variable done;
|
||
|
|
std::optional<T> value;
|
||
|
|
|
||
|
|
void Set(T result) {
|
||
|
|
{
|
||
|
|
std::lock_guard lock(mutex);
|
||
|
|
value = std::move(result);
|
||
|
|
}
|
||
|
|
done.notify_all();
|
||
|
|
}
|
||
|
|
std::optional<T> Wait() {
|
||
|
|
std::unique_lock lock(mutex);
|
||
|
|
done.wait_for(lock, std::chrono::seconds(5), [&] { return value.has_value(); });
|
||
|
|
return value;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
std::string Text(const std::vector<char>& bytes) {
|
||
|
|
return std::string(bytes.begin(), bytes.end());
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
int main() {
|
||
|
|
ThreadPool::Start();
|
||
|
|
constexpr std::uint16_t port = 9188;
|
||
|
|
constexpr std::string_view alpn = "crafter/test-closed-stream";
|
||
|
|
|
||
|
|
QUICServerCredentials serverCreds;
|
||
|
|
serverCreds.selfSigned = true;
|
||
|
|
ListenerQUIC listener(port, std::string(alpn), serverCreds, [&](ClientQUIC* peer) {
|
||
|
|
peer->OnStream([](QUICStream stream) {
|
||
|
|
try {
|
||
|
|
(void)stream.RecieveSync();
|
||
|
|
stream.SendSync("abc", 3, true);
|
||
|
|
} catch (...) {}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
listener.ListenAsyncAsync();
|
||
|
|
|
||
|
|
try {
|
||
|
|
// ── A stream that was never opened ───────────────────────────────────
|
||
|
|
{
|
||
|
|
QUICStream closed;
|
||
|
|
auto received = std::make_shared<Outcome<std::string>>();
|
||
|
|
closed.RecieveAsync([received](std::vector<char> chunk) { received->Set(Text(chunk)); });
|
||
|
|
const std::optional<std::string> got = received->Wait();
|
||
|
|
Check(got && got->empty(), "a receive on a closed stream gets an empty chunk, and the program carries on");
|
||
|
|
auto sent = std::make_shared<Outcome<bool>>();
|
||
|
|
closed.SendAsync("x", 1, false, [sent](bool ok) { sent->Set(ok); });
|
||
|
|
const std::optional<bool> ok = sent->Wait();
|
||
|
|
Check(ok && !*ok, "a send on it tells its callback the stream didn't take it");
|
||
|
|
}
|
||
|
|
|
||
|
|
QUICClientCredentials clientCreds;
|
||
|
|
clientCreds.insecureNoServerValidation = true;
|
||
|
|
ClientQUIC client("localhost", port, std::string(alpn), clientCreds);
|
||
|
|
|
||
|
|
// ── The server answers, then closes its side ─────────────────────────
|
||
|
|
{
|
||
|
|
QUICStream stream = client.OpenStream();
|
||
|
|
auto sent = std::make_shared<Outcome<bool>>();
|
||
|
|
stream.SendAsync("hi", 2, false, [sent](bool ok) { sent->Set(ok); });
|
||
|
|
const std::optional<bool> ok = sent->Wait();
|
||
|
|
Check(ok && *ok, "a send the stream takes tells its callback so");
|
||
|
|
|
||
|
|
auto first = std::make_shared<Outcome<std::string>>();
|
||
|
|
stream.RecieveUntilFullAsync(10, [first](std::vector<char> chunk) { first->Set(Text(chunk)); });
|
||
|
|
const std::optional<std::string> got = first->Wait();
|
||
|
|
Check(got == "abc", std::format("RecieveUntilFullAsync on a stream that closes early gets what arrived ({})", got.value_or("nothing")));
|
||
|
|
|
||
|
|
auto after = std::make_shared<Outcome<std::string>>();
|
||
|
|
stream.RecieveAsync([after](std::vector<char> chunk) { after->Set(Text(chunk)); });
|
||
|
|
const std::optional<std::string> next = after->Wait();
|
||
|
|
Check(next && next->empty(), "a receive after the close gets an empty chunk");
|
||
|
|
}
|
||
|
|
{
|
||
|
|
QUICStream stream = client.OpenStream();
|
||
|
|
stream.SendSync("hi", 2, false);
|
||
|
|
bool threw = false;
|
||
|
|
try {
|
||
|
|
(void)stream.RecieveUntilFullSync(10);
|
||
|
|
} catch (const QUICClosedException&) {
|
||
|
|
threw = true;
|
||
|
|
}
|
||
|
|
const std::vector<char> left = stream.RecieveUntilCloseSync();
|
||
|
|
Check(threw && Text(left) == "abc",
|
||
|
|
std::format("RecieveUntilFullSync throws on the early close and leaves the bytes for the next receive ({})", Text(left)));
|
||
|
|
}
|
||
|
|
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);
|
||
|
|
} catch (const std::exception& e) {
|
||
|
|
std::println("FAIL {}", e.what());
|
||
|
|
std::fflush(stdout);
|
||
|
|
std::_Exit(1);
|
||
|
|
}
|
||
|
|
}
|