test(optimizerclient) : le moteur face aux règles 1.2 — fermeture, lots, longueur de ligne
Preuves, côté moteur, des points Q4, Q7 et Q8 du protocole 1.2. Aucun code du moteur n'est modifié. - testARefusedHandshakeIsClosedByTheEngine (Q4) : -32001 ou -32602 au handshake, faux optimiseur qui garde son socket ouvert → c'est le moteur qui ferme (handshake-refused), nouvelle tentative au backoff seulement. - testAReceivedBatchIsNeverUnpacked (Q8, reçu) : un lot portant la réponse attendue est une ligne écartée (invalid-json), connexion conservée, réponse non lue ; la requête finit en délai dépassé. - testEachLineSentIsOneObject (Q8, émis) : lu sur les octets, chaque ligne émise est UN objet JSON, une requête remplacée donne une ligne de plus. - testAContextOverTheLineLimitIsStillSent (Q7, émis) : ÉCART constaté, non corrigé dans ce lot — requestPlan() ne borne pas ce qu'il émet ; un GetPlan de plus de 1 Mio part tel quel. QEXPECT_FAIL : le test passera en XPASS le jour où le moteur bornera l'émission. Q7 en réception était déjà éprouvé par testLineLengthLimit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e56485d014
commit
952f64d24b
@ -52,6 +52,37 @@ int countLog(const QString &needle)
|
||||
return n;
|
||||
}
|
||||
|
||||
/*! Optimiseur BRUT : ce que le client écrit, octet par octet, sans le découpage du faux
|
||||
* optimiseur (qui ne rend que des objets, et ne verrait donc pas un lot). */
|
||||
struct RawOptimizer
|
||||
{
|
||||
QLocalServer server;
|
||||
QLocalSocket *socket = nullptr;
|
||||
QByteArray received;
|
||||
|
||||
QString listen(const QString &path)
|
||||
{
|
||||
QLocalServer::removeServer(path);
|
||||
return server.listen(path) ? QStringLiteral("unix://") + path : QString();
|
||||
}
|
||||
//! Lignes complètes reçues jusqu'ici, dans l'ordre (sans leur '\n').
|
||||
QList<QByteArray> lines()
|
||||
{
|
||||
if (!socket && server.hasPendingConnections())
|
||||
socket = server.nextPendingConnection();
|
||||
if (socket)
|
||||
received += socket->readAll();
|
||||
QList<QByteArray> l = received.split('\n');
|
||||
l.removeLast(); // ce qui suit le dernier '\n' : vide, ou une ligne pas encore finie
|
||||
return l;
|
||||
}
|
||||
void send(const QByteArray &line)
|
||||
{
|
||||
socket->write(line + '\n');
|
||||
socket->flush();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
/*! Horloge simulée : le temps n'avance que par advance(), et les réveils échus sonnent dans
|
||||
@ -112,6 +143,11 @@ private slots:
|
||||
void testWriteToken();
|
||||
void testTokenNeverInAnyLog();
|
||||
void testAShortTokenFileOpensNoConnection();
|
||||
void testARefusedHandshakeIsClosedByTheEngine_data();
|
||||
void testARefusedHandshakeIsClosedByTheEngine();
|
||||
void testAReceivedBatchIsNeverUnpacked();
|
||||
void testEachLineSentIsOneObject();
|
||||
void testAContextOverTheLineLimitIsStillSent();
|
||||
|
||||
private:
|
||||
// Démarre le client et mène le handshake jusqu'à Ready.
|
||||
@ -847,5 +883,133 @@ void TestOptimizerClient::testAShortTokenFileOpensNoConnection()
|
||||
QCOMPARE(server.connections, 1);
|
||||
}
|
||||
|
||||
void TestOptimizerClient::testARefusedHandshakeIsClosedByTheEngine_data()
|
||||
{
|
||||
QTest::addColumn<int>("code");
|
||||
QTest::newRow("token refusé, -32001") << -32001;
|
||||
QTest::newRow("majeure incompatible, -32602") << -32602;
|
||||
}
|
||||
|
||||
void TestOptimizerClient::testARefusedHandshakeIsClosedByTheEngine()
|
||||
{
|
||||
// Protocole 1.2 (Q4) : après un handshake refusé, les DEUX côtés ferment. Ce faux optimiseur
|
||||
// envoie l'erreur et GARDE son socket ouvert : la déconnexion qu'il constate vient du moteur.
|
||||
QFETCH(int, code);
|
||||
FakeOptimizer server(FakeOptimizer::Unix, m_dir.path());
|
||||
QVERIFY(server.listen());
|
||||
SimClock clock;
|
||||
OptimizerClient client(OptimizerEndpoint::parse(server.endpoint()), QString(), "9.9", &clock);
|
||||
|
||||
client.start();
|
||||
QJsonObject hs;
|
||||
QVERIFY(server.waitMessage(&hs));
|
||||
server.sendJson(QJsonObject{{"jsonrpc", "2.0"}, {"id", hs.value("id")},
|
||||
{"error", QJsonObject{{"code", code}, {"message", "refusé"}}}});
|
||||
QVERIFY(waitFor([&] { return server.disconnections == 1; }));
|
||||
QCOMPARE(client.state(), OptimizerClient::Waiting);
|
||||
QCOMPARE(client.lastErrorCode(), QStringLiteral("handshake-refused"));
|
||||
QCOMPARE(client.requestPlan({}), quint64(0));
|
||||
// Nouvelle tentative au backoff, pas avant : la fermeture n'est pas une reconnexion immédiate.
|
||||
settle();
|
||||
QCOMPARE(server.connections, 1);
|
||||
QCOMPARE(clock.remaining(), qint64(1000));
|
||||
}
|
||||
|
||||
void TestOptimizerClient::testAReceivedBatchIsNeverUnpacked()
|
||||
{
|
||||
// Protocole 1.2 (Q8) : lots interdits dans les deux sens. REÇU, un lot est une ligne écartée
|
||||
// (invalid-json : ce n'est pas un objet), comptée, connexion CONSERVÉE ; la réponse qu'il
|
||||
// contient n'est pas lue. La requête en vol attend donc toujours, et son délai la compte en échec.
|
||||
FakeOptimizer server(FakeOptimizer::Unix, m_dir.path());
|
||||
QVERIFY(server.listen());
|
||||
SimClock clock;
|
||||
OptimizerClient client(OptimizerEndpoint::parse(server.endpoint()), QString(), "9.9", &clock);
|
||||
bringUp(client, server);
|
||||
QSignalSpy planSpy(&client, &OptimizerClient::planReceived);
|
||||
QSignalSpy failSpy(&client, &OptimizerClient::planFailed);
|
||||
QSignalSpy errorSpy(&client, &OptimizerClient::protocolError);
|
||||
|
||||
const quint64 id = client.requestPlan({});
|
||||
QJsonObject req;
|
||||
QVERIFY(server.waitMessage(&req));
|
||||
const QByteArray reponse = QJsonDocument(QJsonObject{{"jsonrpc", "2.0"}, {"id", qint64(id)},
|
||||
{"result", QJsonObject{{"planId", "dans-un-lot"}}}})
|
||||
.toJson(QJsonDocument::Compact);
|
||||
server.sendRaw("[" + reponse + "]\n");
|
||||
QVERIFY(waitFor([&] { return errorSpy.count() == 1; }));
|
||||
QCOMPARE(errorSpy.at(0).at(0).toString(), QStringLiteral("invalid-json"));
|
||||
settle();
|
||||
QCOMPARE(planSpy.count(), 0);
|
||||
QCOMPARE(client.pendingRequestId(), id);
|
||||
QCOMPARE(client.state(), OptimizerClient::Ready);
|
||||
|
||||
clock.advance(OptimizerClient::kRequestTimeoutMs);
|
||||
QCOMPARE(failSpy.count(), 1);
|
||||
QCOMPARE(failSpy.at(0).at(1).toString(), QStringLiteral("timeout"));
|
||||
settle();
|
||||
QCOMPARE(client.state(), OptimizerClient::Ready);
|
||||
QCOMPARE(server.disconnections, 0);
|
||||
}
|
||||
|
||||
void TestOptimizerClient::testEachLineSentIsOneObject()
|
||||
{
|
||||
// Protocole 1.2 (Q8, sens moteur → optimiseur) : un message par ligne, jamais un lot. Le client
|
||||
// n'a qu'un point d'écriture, send(), qui sérialise UN objet ; vérifié ici sur les octets.
|
||||
RawOptimizer raw;
|
||||
const QString endpoint = raw.listen(m_dir.filePath("brut.sock"));
|
||||
QVERIFY(!endpoint.isEmpty());
|
||||
SimClock clock;
|
||||
OptimizerClient client(OptimizerEndpoint::parse(endpoint), QString(), "9.9", &clock);
|
||||
|
||||
client.start();
|
||||
QVERIFY(waitFor([&] { return raw.lines().size() == 1; }));
|
||||
raw.send(R"({"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"1.2"}})");
|
||||
QVERIFY(waitFor([&] { return client.state() == OptimizerClient::Ready; }));
|
||||
const QVariantMap context{{"timestamp", "2026-10-16T10:00:00Z"},
|
||||
{"loads", QVariantList{QVariantMap{{"id", "a"}}, QVariantMap{{"id", "b"}}}}};
|
||||
QCOMPARE(client.requestPlan(context), quint64(2));
|
||||
QCOMPARE(client.requestPlan(context), quint64(3)); // remplace la précédente : une ligne de plus, pas un lot
|
||||
QVERIFY(waitFor([&] { return raw.lines().size() == 3; }));
|
||||
|
||||
const QList<QByteArray> lines = raw.lines();
|
||||
QVERIFY(raw.received.endsWith('\n'));
|
||||
for (int i = 0; i < lines.size(); ++i) {
|
||||
QJsonParseError e;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(lines.at(i), &e);
|
||||
QVERIFY2(e.error == QJsonParseError::NoError && doc.isObject() && lines.at(i).startsWith('{'),
|
||||
qPrintable(QStringLiteral("ligne %1 : pas un objet JSON seul").arg(i + 1)));
|
||||
QCOMPARE(doc.object().value("id").toInteger(), qint64(i + 1));
|
||||
QVERIFY(lines.at(i).size() <= OptimizerClient::kMaxLineBytes);
|
||||
}
|
||||
}
|
||||
|
||||
void TestOptimizerClient::testAContextOverTheLineLimitIsStillSent()
|
||||
{
|
||||
// ÉCART au protocole 1.2 (Q7, maxLineBytes dans les deux sens), constaté et NON CORRIGÉ dans le
|
||||
// lot qui norme la règle : requestPlan() n'applique pas kMaxLineBytes à ce qu'il ÉMET. Rien ne
|
||||
// borne non plus la taille d'un contexte en amont — un `label` de charge saisi par SetLoadConfig
|
||||
// suffit. Un optimiseur conforme jette la ligne (-32600) et la requête finit en délai dépassé.
|
||||
// Le jour où le moteur borne l'émission, ce test passe (XPASS) et doit être réécrit.
|
||||
RawOptimizer raw;
|
||||
const QString endpoint = raw.listen(m_dir.filePath("brut.sock"));
|
||||
QVERIFY(!endpoint.isEmpty());
|
||||
SimClock clock;
|
||||
OptimizerClient client(OptimizerEndpoint::parse(endpoint), QString(), "9.9", &clock);
|
||||
|
||||
client.start();
|
||||
QVERIFY(waitFor([&] { return raw.lines().size() == 1; }));
|
||||
raw.send(R"({"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"1.2"}})");
|
||||
QVERIFY(waitFor([&] { return client.state() == OptimizerClient::Ready; }));
|
||||
const QVariantMap context{{"loads", QVariantList{QVariantMap{
|
||||
{"id", "ecs"}, {"label", QString(OptimizerClient::kMaxLineBytes, QLatin1Char('x'))}}}}};
|
||||
QCOMPARE(client.requestPlan(context), quint64(2));
|
||||
QVERIFY(waitFor([&] { return raw.lines().size() == 2; }, 10000));
|
||||
const QByteArray line = raw.lines().at(1);
|
||||
QVERIFY(line.contains("\"GetPlan\""));
|
||||
QEXPECT_FAIL("", "écart Q7 : le moteur émet une ligne de plus de maxLineBytes (rapport protocole 1.2)", Continue);
|
||||
QVERIFY2(line.size() <= OptimizerClient::kMaxLineBytes,
|
||||
qPrintable(QStringLiteral("GetPlan émis sur %1 octets").arg(line.size())));
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(TestOptimizerClient)
|
||||
#include "testoptimizerclient.moc"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user