autotests fixes
tests / package-smoke-linux (push) Skipped
tests / package-smoke-windows (push) Skipped
tests / macos (push) Successful in 1m22s
tests / linux (pull_request) Successful in 1m54s
tests / package-smoke-macos (push) Skipped
tests / linux (push) Successful in 2m43s
tests / windows (push) Successful in 4m13s
tests / windows (pull_request) Skipped
tests / macos (pull_request) Skipped
tests / package-smoke-linux (pull_request) Skipped
tests / package-smoke-windows (pull_request) Skipped
tests / package-smoke-macos (pull_request) Skipped

Signed-off-by: galeon <dertiellis@gmail.com>
This commit is contained in:
galeon committed 2026-11-01 21:07:03 +03:00
1 parent 6a2b596f32
commit 5635dcc558
11 files changed
+134 -52

No files matched your search

+43 -11
View File
@@ -201,12 +201,32 @@ void FakeInternet::unroute(const QString &pattern)
void FakeInternet::reset()
{
m_deferred.clear();
m_diagnostics.clear();
m_routes.clear();
m_requests.clear();
m_unmatched.clear();
m_fallback = FakeResponse::error(404, "fake internet: no route");
}
bool FakeInternet::releaseResponse(const QString &url)
{
for (qsizetype i = 0; i < m_deferred.size(); ++i) {
if (m_deferred.at(i).url == url) {
const auto pending = m_deferred.takeAt(i);
respond(pending.socket, pending.response, pending.keepAlive);
return true;
}
}
return false;
}
QString FakeInternet::diagnostics() const
{
return QStringLiteral("TLS backend: %1\nRequests: %2\n%3")
.arg(QSslSocket::activeBackend(), requestedUrls().join(u' '), m_diagnostics.join(u'\n'));
}
QList<FakeRequest> FakeInternet::requests() const
{
return m_requests;
@@ -366,9 +386,13 @@ void FakeInternet::dispatch(Connection *connection, FakeRequest request)
}
const bool keepAlive = request.header("connection").toLower() != "close";
QPointer<QTcpSocket> socket(connection->socket);
QPointer<QSslSocket> socket(connection->socket);
if (response.hang)
return;
if (response.deferred) {
m_deferred.append({url, socket, response, keepAlive});
return;
}
if (response.delayMs > 0) {
QTimer::singleShot(response.delayMs, this,
[this, socket, response, keepAlive] { respond(socket, response, keepAlive); });
@@ -378,7 +402,7 @@ void FakeInternet::dispatch(Connection *connection, FakeRequest request)
emit requestServed(url);
}
void FakeInternet::respond(QPointer<QTcpSocket> socket, const FakeResponse &response, bool keepAlive)
void FakeInternet::respond(QPointer<QSslSocket> socket, const FakeResponse &response, bool keepAlive)
{
if (!socket || socket->state() != QAbstractSocket::ConnectedState)
return;
@@ -397,20 +421,28 @@ void FakeInternet::respond(QPointer<QTcpSocket> socket, const FakeResponse &resp
head += close ? "Connection: close\r\n" : "Connection: keep-alive\r\n";
head += "\r\n";
socket->write(head);
if (response.truncateTo >= 0) {
socket->write(response.body.left(response.truncateTo));
socket->flush();
socket->disconnectFromHost();
// Schannel (Windows) на disconnectFromHost шлёт только TLS close_notify и держит
// TCP открытым, ожидая ответного close_notify, — клиент тогда висит до таймаута.
// Обрыв на то и обрыв: если сокет не закрылся сам, рвём соединение.
QTimer::singleShot(200, socket, [socket] {
if (socket && socket->state() != QAbstractSocket::UnconnectedState)
m_diagnostics << QStringLiteral("truncate: declared=%1, sent=%2")
.arg(response.body.size()).arg(response.body.left(response.truncateTo).size());
// Сначала отправляем TLS-записи с неполным телом, затем рвём TCP.
// disconnectFromHost() выполняет штатное завершение TLS, а здесь
// моделируется обрыв. Готовность определяется буферами, не задержкой.
connect(socket, &QSslSocket::encryptedBytesWritten, socket, [this, socket](qint64) {
QTimer::singleShot(0, socket, [this, socket] {
if (!socket || socket->state() != QAbstractSocket::ConnectedState)
return;
if (socket->bytesToWrite() || socket->encryptedBytesToWrite())
return;
m_diagnostics << QStringLiteral("truncate: TLS buffers drained, aborting TCP");
socket->abort();
m_diagnostics << QStringLiteral("truncate: state after abort=%1").arg(socket->state());
});
});
socket->write(head);
socket->write(response.body.left(response.truncateTo));
return;
}
socket->write(head);
socket->write(response.body);
if (close)
socket->disconnectFromHost();
+13 -1
View File
@@ -56,6 +56,7 @@ struct FakeResponse
QList<QPair<QByteArray, QByteArray>> headers;
int delayMs = 0; // ответить с задержкой
bool hang = false; // не отвечать вовсе (для отмены и таймаутов)
bool deferred = false; // ждать явного releaseResponse(), без таймера
bool drop = false; // оборвать соединение без ответа
int truncateTo = -1; // объявить полный размер, отдать столько байт и оборвать
@@ -95,6 +96,8 @@ public:
// Сбросить маршруты и журнал.
void reset();
bool releaseResponse(const QString &url);
QString diagnostics() const;
QList<FakeRequest> requests() const;
// Сколько раз был запрошен адрес "https://host/path" (query не учитывается).
@@ -122,7 +125,7 @@ private:
void onReadyRead(Connection *connection);
void processBuffer(Connection *connection);
void dispatch(Connection *connection, FakeRequest request);
void respond(QPointer<QTcpSocket> socket, const FakeResponse &response, bool keepAlive);
void respond(QPointer<QSslSocket> socket, const FakeResponse &response, bool keepAlive);
void startTls(Connection *connection);
const Route *match(const FakeRequest &request) const;
@@ -130,6 +133,15 @@ private:
FakeResponse m_fallback;
QList<FakeRequest> m_requests;
QStringList m_unmatched;
struct DeferredResponse
{
QString url;
QPointer<QSslSocket> socket;
FakeResponse response;
bool keepAlive;
};
QList<DeferredResponse> m_deferred;
QStringList m_diagnostics;
QByteArray m_certificate;
QByteArray m_key;
bool m_started = false;
+1 -1
View File
@@ -8,7 +8,7 @@
#include <QtTest/QTest>
// В initTestCase(): поднять FakeInternet или пропустить весь тест-класс.
// С BILAUNCHER_REQUIRE_FAKE_INTERNET=1 (так в CI на Linux) недоступность —
// С BILAUNCHER_REQUIRE_FAKE_INTERNET=1 (так в CI на всех ОС) недоступность —
// ошибка, а не пропуск: иначе сломанный перехват тихо выключил бы все сетевые тесты.
#define BILAUNCHER_START_FAKE_INTERNET(net) \
do { \
+24 -10
View File
@@ -3,6 +3,7 @@
#include "testenv.h"
#include "fakeinternet.h"
#include "nettest.h"
#include <QNetworkAccessManager>
#include <QNetworkReply>
@@ -30,9 +31,7 @@ class TestFakeInternet : public QObject
private slots:
void initTestCase()
{
QString error;
if (!m_net.start(&error))
QSKIP(qPrintable(QStringLiteral("FakeInternet недоступен: ") + error));
BILAUNCHER_START_FAKE_INTERNET(m_net);
}
void init() { m_net.reset(); }
@@ -100,12 +99,6 @@ private slots:
response.drop = true;
return response;
}());
m_net.route(QStringLiteral("https://a.test/short"), [] {
FakeResponse response = FakeResponse::bytes(QByteArray(1000, 'x'));
response.truncateTo = 10;
return response;
}());
QNetworkAccessManager manager;
QScopedPointer<QNetworkReply> r500(get(manager, QStringLiteral("https://a.test/500")));
QVERIFY(waitFinished(r500.data()));
@@ -114,10 +107,31 @@ private slots:
QScopedPointer<QNetworkReply> drop(get(manager, QStringLiteral("https://a.test/drop")));
QVERIFY(waitFinished(drop.data()));
QVERIFY(drop->error() != QNetworkReply::NoError);
}
void truncatedResponse_data()
{
QTest::addColumn<int>("bytes");
QTest::newRow("empty") << 0;
QTest::newRow("short") << 10;
QTest::newRow("multiple-tls-records") << 65536;
QTest::newRow("last-byte-missing") << 256 * 1024 - 1;
}
void truncatedResponse()
{
QFETCH(int, bytes);
const QByteArray body(256 * 1024, 'x');
FakeResponse cut = FakeResponse::bytes(body);
cut.truncateTo = bytes;
m_net.route(QStringLiteral("https://a.test/short"), cut);
QNetworkAccessManager manager;
QScopedPointer<QNetworkReply> shortReply(get(manager, QStringLiteral("https://a.test/short")));
QVERIFY(waitFinished(shortReply.data()));
QVERIFY2(waitFinished(shortReply.data()),
qPrintable(m_net.diagnostics() + "\nClient: " + shortReply->errorString()));
QVERIFY(shortReply->error() != QNetworkReply::NoError);
QCOMPARE(shortReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), 200);
QCOMPARE(shortReply->readAll(), body.left(bytes));
}
void plainHttpThroughProxy()
+33 -15
View File
@@ -7,6 +7,8 @@
#include "modloaderversionservice.h"
#include <QSignalSpy>
#include <QNetworkAccessManager>
#include <QNetworkReply>
namespace {
const QString kFabric = QStringLiteral("https://meta.fabricmc.net/v2/versions/loader/1.20.1");
@@ -55,6 +57,25 @@ class TestModLoaderVersionService : public QObject
return QTest::qWaitFor([&] { return r.calls > 0; }, 15000);
}
bool loadNeoForgeInOrder(ModLoaderVersionService &service, CallResult &result,
const QString &first, const QString &second)
{
QStringList completed;
QObject observer;
auto *network = service.findChild<QNetworkAccessManager *>();
if (!network)
qFatal("ModLoaderVersionService has no network manager");
connect(network, &QNetworkAccessManager::finished, &observer, [&](QNetworkReply *reply) {
completed << reply->url().toString();
});
service.ensureLoaded(ModLoader::NeoForge, "1.20.1", result.callback());
if (!QTest::qWaitFor([&] { return completed.contains(first) && m_net.hits(second) == 1; }, 15000))
return false;
if (!m_net.releaseResponse(second))
return false;
return QTest::qWaitFor([&] { return result.calls == 1; }, 15000);
}
private slots:
void initTestCase() { BILAUNCHER_START_FAKE_INTERNET(m_net); }
@@ -167,33 +188,30 @@ private slots:
void neoForgeLegacyArtifactArrivesLast()
{
// Ранние сборки под 1.20.1 — из старого артефакта net.neoforged:forge.
FakeResponse legacySlow = xml(Fixtures::read("loaders/neoforge-legacy-maven-metadata.xml"));
legacySlow.delayMs = 400;
m_net.route(kNeoLegacyMeta, legacySlow);
FakeResponse legacy = xml(Fixtures::read("loaders/neoforge-legacy-maven-metadata.xml"));
legacy.deferred = true;
m_net.route(kNeoLegacyMeta, legacy);
ModLoaderVersionService service;
CallResult r;
QVERIFY(load(service, ModLoader::NeoForge, "1.20.1", r));
QVERIFY(loadNeoForgeInOrder(service, r, kNeoMeta, kNeoLegacyMeta));
QVERIFY(r.ok);
const QList<LoaderVersionEntry> legacy = service.versions(ModLoader::NeoForge, "1.20.1");
QCOMPARE(loaderVersions(legacy), QStringList({"47.1.106", "47.1.79"}));
QCOMPARE(legacy.at(0).installerUrl,
const QList<LoaderVersionEntry> versions = service.versions(ModLoader::NeoForge, "1.20.1");
QCOMPARE(loaderVersions(versions), QStringList({"47.1.106", "47.1.79"}));
QCOMPARE(versions.at(0).installerUrl,
QUrl("https://maven.neoforged.net/releases/net/neoforged/forge/1.20.1-47.1.106/forge-1.20.1-47.1.106-installer.jar"));
}
void neoForgeLegacyArtifactArrivesFirst()
{
// Тот же каталог, но маленький legacy-ответ приходит раньше основного.
FakeResponse primarySlow = xml(Fixtures::read("loaders/neoforge-maven-metadata.xml"));
primarySlow.delayMs = 400;
m_net.route(kNeoMeta, primarySlow);
// Основной ответ выпускается только после обработки legacy клиентом.
FakeResponse primary = xml(Fixtures::read("loaders/neoforge-maven-metadata.xml"));
primary.deferred = true;
m_net.route(kNeoMeta, primary);
ModLoaderVersionService service;
CallResult r;
QVERIFY(load(service, ModLoader::NeoForge, "1.20.1", r));
QVERIFY(loadNeoForgeInOrder(service, r, kNeoLegacyMeta, kNeoMeta));
QVERIFY(r.ok);
QCOMPARE(service.versions(ModLoader::NeoForge, "1.21.1").size(), 2);
// В startRefresh() основной ответ делает state->merged = parsed и затирает
// уже слитые записи legacy — версии NeoForge для 1.20.1 пропадают.
QEXPECT_FAIL("", "гонка: основной maven-metadata NeoForge затирает уже пришедший legacy", Continue);
QCOMPARE(service.versions(ModLoader::NeoForge, "1.20.1").size(), 2);
}
+1 -1
View File
@@ -94,7 +94,7 @@ private slots:
m_net.route(kPack, cut);
Run run(downloader);
downloader.download(QUrl(kPack), m_dir + "/b.zip", Fixtures::sha256(QByteArray(4096, 'x')), 4096, "B");
QVERIFY(run.waitEnd());
QVERIFY2(run.waitEnd(), qPrintable(m_net.diagnostics()));
QCOMPARE(run.failed.count(), 1);
QVERIFY(!QFile::exists(m_dir + "/b.zip"));
}
+3 -2
View File
@@ -34,8 +34,9 @@ public slots:
seedData();
// Диалоги создают настоящий LauncherBackend — наружу он не должен ходить.
QString error;
if (m_net.start(&error))
m_net.setFallback(FakeResponse::error(503));
if (!m_net.start(&error))
qFatal("FakeInternet: %s", qPrintable(error));
m_net.setFallback(FakeResponse::error(503));
}
void qmlEngineAvailable(QQmlEngine *engine)
+2 -2
View File
@@ -49,8 +49,8 @@ private slots:
void initTestCase()
{
QString error;
if (m_net.start(&error))
m_net.setFallback(FakeResponse::error(503));
QVERIFY2(m_net.start(&error), qPrintable(error));
m_net.setFallback(FakeResponse::error(503));
const QString game = TestEnv::freshDir(QStringLiteral("game"));
Fixtures::writeJson(LauncherPaths::settingsFile(), QJsonObject{{"gameDir", game}, {"language", "en"}});
QQuickStyle::setStyle(QStringLiteral("Basic"));