93 lines
3.5 KiB
C++
93 lines
3.5 KiB
C++
|
|
//SPDX-License-Identifier: LGPL-3.0-only
|
||
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
// A failed resolve or connect throws SocketErrorException, which carries the
|
||
|
|
// code it failed with: the errno for a connect, the getaddrinfo result for a
|
||
|
|
// name. Before, both were a bare std::runtime_error, so a caller that reports
|
||
|
|
// the cause in its own words — a Python-style "[Errno 111] Connection
|
||
|
|
// refused" in front of a language model reading the output — had to parse the
|
||
|
|
// message back apart. The message itself stays what it always was, and the
|
||
|
|
// exception is still a std::runtime_error, so existing handlers keep working.
|
||
|
|
|
||
|
|
#include <arpa/inet.h>
|
||
|
|
#include <netdb.h>
|
||
|
|
#include <netinet/in.h>
|
||
|
|
#include <sys/socket.h>
|
||
|
|
#include <unistd.h>
|
||
|
|
#include <cerrno>
|
||
|
|
#include <cstdlib>
|
||
|
|
#include <cstring>
|
||
|
|
|
||
|
|
import Crafter.Network;
|
||
|
|
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 port on 127.0.0.1 nothing listens on: bound by the kernel, then released.
|
||
|
|
std::uint16_t ClosedPort() {
|
||
|
|
const int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||
|
|
sockaddr_in address {};
|
||
|
|
address.sin_family = AF_INET;
|
||
|
|
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||
|
|
bind(fd, reinterpret_cast<sockaddr*>(&address), sizeof(address));
|
||
|
|
socklen_t length = sizeof(address);
|
||
|
|
getsockname(fd, reinterpret_cast<sockaddr*>(&address), &length);
|
||
|
|
close(fd);
|
||
|
|
return ntohs(address.sin_port);
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
int main() {
|
||
|
|
{
|
||
|
|
bool thrown = false;
|
||
|
|
try {
|
||
|
|
ClientTCP client("127.0.0.1", ClosedPort(), 2s);
|
||
|
|
} catch (const SocketErrorException& e) {
|
||
|
|
thrown = true;
|
||
|
|
Check(e.kind == SocketErrorKind::System, "a refused connect is a system error");
|
||
|
|
Check(e.code == ECONNREFUSED, std::format("it carries ECONNREFUSED (got {})", e.code));
|
||
|
|
Check(std::string_view(e.what()) == std::string("Could not connect to server: ") + std::strerror(ECONNREFUSED),
|
||
|
|
std::format("its message is the one ClientTCP always threw (got '{}')", e.what()));
|
||
|
|
}
|
||
|
|
Check(thrown, "a refused connect throws SocketErrorException");
|
||
|
|
}
|
||
|
|
|
||
|
|
{
|
||
|
|
bool caught = false;
|
||
|
|
try {
|
||
|
|
ClientTCP client("127.0.0.1", ClosedPort(), 2s);
|
||
|
|
} catch (const std::runtime_error&) {
|
||
|
|
caught = true;
|
||
|
|
}
|
||
|
|
Check(caught, "it is still caught as std::runtime_error");
|
||
|
|
}
|
||
|
|
|
||
|
|
{
|
||
|
|
// .invalid never resolves (RFC 6761); without a network the resolver
|
||
|
|
// fails differently (EAI_AGAIN), but always as a resolver error.
|
||
|
|
bool thrown = false;
|
||
|
|
try {
|
||
|
|
ClientTCP client("no-such-host.invalid", 80, 2s);
|
||
|
|
} catch (const SocketErrorException& e) {
|
||
|
|
thrown = true;
|
||
|
|
Check(e.kind == SocketErrorKind::Resolver, "a name that does not resolve is a resolver error");
|
||
|
|
Check(e.code != 0, std::format("it carries the getaddrinfo code (got {})", e.code));
|
||
|
|
Check(std::string_view(e.what()).starts_with("Could not resolve host 'no-such-host.invalid': "),
|
||
|
|
std::format("its message names the host (got '{}')", e.what()));
|
||
|
|
}
|
||
|
|
Check(thrown, "an unresolvable host throws SocketErrorException");
|
||
|
|
}
|
||
|
|
|
||
|
|
std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed");
|
||
|
|
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
||
|
|
}
|