test(conformance) : sonde des règles 1.2 que le moteur ne provoque pas
Quatre fonctions, une connexion de SONDE sur l'optimiseur (sans le moteur), pour ce que le client du moteur n'envoie jamais : - testGetPlanBeforeTheHandshakeIsRefused (Q3) : -32600 à son id, puis le handshake sur la même connexion est accepté (connexion gardée) ; - testARefusedHandshakeIsClosedByTheOptimizer (Q3, Q4) : autre majeure → -32602 puis l'optimiseur ferme ; avec un token attendu, token faux → -32001, fermé pareil ; - testABatchIsRefused (Q8) : un lot → une erreur -32600 à id null, son contenu non traité, connexion gardée ; - testALineOverTheLimitIsRefused (Q7) : maxLineBytes + 1 → -32600 à id null, une ligne d'exactement maxLineBytes est lue. Le traitement d'un message se constate par une réponse normée sans contexte : UpdateLearned avec id → -32601 (Q6). Réponses lues brutes : un lot renvoyé se verrait. README : tableau, boucle de lancement, ce qui n'est pas vérifié. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4cbbb81802
commit
7bb2b4a194
@ -11,9 +11,18 @@ adaptateurs, sur les mocks de la suite `simulation` — et vérifie, une fonctio
|
|||||||
| `testTheSessionMatchesTheContract` | la session entière (les deux sens) est conforme au contrat OpenRPC **embarqué** dans le moteur, règles sémantiques comprises (`tools/etm-powersync-schema-check --semantic`) |
|
| `testTheSessionMatchesTheContract` | la session entière (les deux sens) est conforme au contrat OpenRPC **embarqué** dans le moteur, règles sémantiques comprises (`tools/etm-powersync-schema-check --semantic`) |
|
||||||
| `testEveryPlanIsAccepted` | à chaque cycle, le plan est **appliqué** : l'optimiseur commande, `GetPlan` sans `fallbackCode` |
|
| `testEveryPlanIsAccepted` | à chaque cycle, le plan est **appliqué** : l'optimiseur commande, `GetPlan` sans `fallbackCode` |
|
||||||
| `testTheSafetyLoopStaysOnTop` | la boucle de sécurité reste au-dessus : une consigne hors bornes (réécrite en 100 kW par le relais de test) est **écrêtée** par l'adaptateur (`clamped`), une surcharge de phase est **délestée** par L4 |
|
| `testTheSafetyLoopStaysOnTop` | la boucle de sécurité reste au-dessus : une consigne hors bornes (réécrite en 100 kW par le relais de test) est **écrêtée** par l'adaptateur (`clamped`), une surcharge de phase est **délestée** par L4 |
|
||||||
|
| `testGetPlanBeforeTheHandshakeIsRefused` | protocole 1.2, §3 : `GetPlan` avant un handshake réussi → erreur `-32600` à son `id`, connexion **gardée** (le handshake qui suit est accepté) |
|
||||||
|
| `testARefusedHandshakeIsClosedByTheOptimizer` | protocole 1.2, §3 : handshake d'une autre majeure → `-32602`, puis l'optimiseur **ferme** ; si un token est attendu, token faux → `-32001`, fermé pareil |
|
||||||
|
| `testABatchIsRefused` | protocole 1.2, §2 : un lot `[…]` → **une** erreur `-32600` à `id: null`, rien du lot traité, connexion gardée |
|
||||||
|
| `testALineOverTheLimitIsRefused` | protocole 1.2, §2 : une ligne de `maxLineBytes` + 1 octets → `-32600` à `id: null`, connexion gardée ; une ligne d'exactement `maxLineBytes` est lue |
|
||||||
|
|
||||||
Ces deux dernières lignes valent pour le mode `plan` (défaut). Voir **Optimiseur qui s'abstient**.
|
Ces deux dernières lignes valent pour le mode `plan` (défaut). Voir **Optimiseur qui s'abstient**.
|
||||||
|
|
||||||
|
Les fonctions 6 à 9 ne branchent pas le moteur : elles envoient, par une **connexion de sonde**, ce
|
||||||
|
que son client n'envoie jamais, et lisent la réponse brute (un lot renvoyé se verrait). Elles
|
||||||
|
utilisent des requêtes dont la réponse est normée sans contexte — `UpdateLearned` avec `id` →
|
||||||
|
`-32601` (§8) — pour constater qu'un message a été, ou n'a pas été, traité.
|
||||||
|
|
||||||
Les fonctions 2 à 5 passent par un relais transparent (une ligne reçue = une ligne renvoyée) qui
|
Les fonctions 2 à 5 passent par un relais transparent (une ligne reçue = une ligne renvoyée) qui
|
||||||
capture la session et mesure les délais ; la 5 est la seule où il réécrit une réponse. Banc : deux
|
capture la session et mesure les délais ; la 5 est la seule où il réécrit une réponse. Banc : deux
|
||||||
charges `etmvariableload` (0–2 000 W ; plancher 800, 3 000 W) et une PAC SG-Ready (P3 1 500 W,
|
charges `etmvariableload` (0–2 000 W ; plancher 800, 3 000 W) et une PAC SG-Ready (P3 1 500 W,
|
||||||
@ -30,7 +39,8 @@ cd <build>/tests/auto/conformance
|
|||||||
export NYMEA_PLUGINS_PATH=<build>/tests/mocks/plugins/energymocks:/usr/lib/x86_64-linux-gnu/nymea/plugins
|
export NYMEA_PLUGINS_PATH=<build>/tests/mocks/plugins/energymocks:/usr/lib/x86_64-linux-gnu/nymea/plugins
|
||||||
export ETM_POWERSYNC_OPTIMIZER_CMD='exec /chemin/de/mon-optimiseur --listen {endpoint}'
|
export ETM_POWERSYNC_OPTIMIZER_CMD='exec /chemin/de/mon-optimiseur --listen {endpoint}'
|
||||||
for fn in testHandshakeIsAccepted testGetPlanIsAnsweredInTime testTheSessionMatchesTheContract \
|
for fn in testHandshakeIsAccepted testGetPlanIsAnsweredInTime testTheSessionMatchesTheContract \
|
||||||
testEveryPlanIsAccepted testTheSafetyLoopStaysOnTop; do
|
testEveryPlanIsAccepted testTheSafetyLoopStaysOnTop testGetPlanBeforeTheHandshakeIsRefused \
|
||||||
|
testARefusedHandshakeIsClosedByTheOptimizer testABatchIsRefused testALineOverTheLimitIsRefused; do
|
||||||
systemd-run --user --scope -q -p MemoryMax=2G \
|
systemd-run --user --scope -q -p MemoryMax=2G \
|
||||||
bwrap --dev-bind / / --unshare-net --tmpfs /tmp --bind "$PWD/../../.." "$PWD/../../.." \
|
bwrap --dev-bind / / --unshare-net --tmpfs /tmp --bind "$PWD/../../.." "$PWD/../../.." \
|
||||||
--bind <sources> <sources> --chdir "$PWD" ./target_wrapper.sh ./etm-powersync-optimizer-conformance "$fn"
|
--bind <sources> <sources> --chdir "$PWD" ./target_wrapper.sh ./etm-powersync-optimizer-conformance "$fn"
|
||||||
@ -65,7 +75,7 @@ toujours est conforme. Avec `ETM_POWERSYNC_CONFORMANCE_EXPECT=abstain` :
|
|||||||
- `testTheSafetyLoopStaysOnTop` vérifie toujours L4 sous le rule-based, sans consigne de
|
- `testTheSafetyLoopStaysOnTop` vérifie toujours L4 sous le rule-based, sans consigne de
|
||||||
l'optimiseur : la trace doit porter `shed` (la seule baisse des consignes ne suffit pas, l'import
|
l'optimiseur : la trace doit porter `shed` (la seule baisse des consignes ne suffit pas, l'import
|
||||||
la produit déjà). Puis la fonction se termine en **SKIP** : aucune consigne à écrêter ;
|
la produit déjà). Puis la fonction se termine en **SKIP** : aucune consigne à écrêter ;
|
||||||
- les trois autres fonctions sont inchangées.
|
- les autres fonctions sont inchangées.
|
||||||
|
|
||||||
À l'inverse, un optimiseur qui s'abstient en mode `plan` échoue à `testEveryPlanIsAccepted` et
|
À l'inverse, un optimiseur qui s'abstient en mode `plan` échoue à `testEveryPlanIsAccepted` et
|
||||||
`testTheSafetyLoopStaysOnTop`. Chaque fonction affiche en tête le mode retenu (`[Conformité] mode
|
`testTheSafetyLoopStaysOnTop`. Chaque fonction affiche en tête le mode retenu (`[Conformité] mode
|
||||||
@ -81,6 +91,9 @@ fondée sur ces champs jugerait faux dans les deux cas : le mode se donne, il ne
|
|||||||
## Ce qu'elle ne vérifie pas
|
## Ce qu'elle ne vérifie pas
|
||||||
|
|
||||||
La **qualité** des plans (aucune comparaison au rule-based : c'est `refoptimizer`, pour la référence
|
La **qualité** des plans (aucune comparaison au rule-based : c'est `refoptimizer`, pour la référence
|
||||||
seulement), les bornes VE (aucune borne au banc), `UpdateLearned` (ignoré par le moteur 1.1), le
|
seulement), les bornes VE (aucune borne au banc), `UpdateLearned` émis par l'optimiseur (ignoré par le moteur ; sa
|
||||||
comportement sous défaut de transport. Le contrat : `docs/etm-powersync-optimizer-protocol.openrpc.json`
|
forme n'est pas normée en 1.2), le token envoyé à un optimiseur qui n'en exige pas (§3, 1.2 : accepté ;
|
||||||
|
le moteur n'envoie de token que si l'installateur en a posé un), la réponse à un contexte `GetPlan`
|
||||||
|
invalide (§7, 1.2 : `-32602` ; la forme d'`error.data` n'est pas normée), le comportement sous défaut
|
||||||
|
de transport. Le contrat : `docs/etm-powersync-optimizer-protocol.openrpc.json`
|
||||||
(prose : `docs/OPTIMIZER_PROTOCOL.md`, annexe G).
|
(prose : `docs/OPTIMIZER_PROTOCOL.md`, annexe G).
|
||||||
|
|||||||
@ -16,7 +16,11 @@
|
|||||||
// sémantiques comprises — jugée par le validateur publié ;
|
// sémantiques comprises — jugée par le validateur publié ;
|
||||||
// 4. chaque plan ACCEPTÉ : l'optimiseur commande, `GetPlan` sans `fallbackCode` ;
|
// 4. chaque plan ACCEPTÉ : l'optimiseur commande, `GetPlan` sans `fallbackCode` ;
|
||||||
// 5. la boucle de sécurité au-dessus : une consigne hors bornes est écrêtée par l'adaptateur,
|
// 5. la boucle de sécurité au-dessus : une consigne hors bornes est écrêtée par l'adaptateur,
|
||||||
// une surcharge de phase est délestée par L4, quoi que dise l'optimiseur.
|
// une surcharge de phase est délestée par L4, quoi que dise l'optimiseur ;
|
||||||
|
// 6 à 9 (protocole 1.2). Ce que le moteur n'envoie JAMAIS, envoyé par une connexion de SONDE
|
||||||
|
// (sans le moteur) : GetPlan avant le handshake (-32600, connexion gardée), handshake d'une
|
||||||
|
// autre majeure (-32602 puis fermeture par l'optimiseur), lot (-32600 à id null, rien du lot
|
||||||
|
// traité), ligne de plus de 1 Mio (-32600 à id null, connexion gardée).
|
||||||
//
|
//
|
||||||
// Mode attendu ($ETM_POWERSYNC_CONFORMANCE_EXPECT, défaut `plan`) : `abstain` juge un optimiseur
|
// Mode attendu ($ETM_POWERSYNC_CONFORMANCE_EXPECT, défaut `plan`) : `abstain` juge un optimiseur
|
||||||
// qui s'abstient toujours (§7, réponse permise) — le point 4 exige alors une abstention à chaque
|
// qui s'abstient toujours (§7, réponse permise) — le point 4 exige alors une abstention à chaque
|
||||||
@ -52,6 +56,7 @@
|
|||||||
#include <QTemporaryDir>
|
#include <QTemporaryDir>
|
||||||
|
|
||||||
#include <csignal>
|
#include <csignal>
|
||||||
|
#include <memory>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
using namespace nymeaserver;
|
using namespace nymeaserver;
|
||||||
@ -319,6 +324,69 @@ private:
|
|||||||
QElapsedTimer m_chrono;
|
QElapsedTimer m_chrono;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Connexion de SONDE (protocole 1.2) : des lignes écrites à la main, les réponses lues une à une,
|
||||||
|
* sans le moteur — pour ce que son client n'envoie jamais. Une réponse est gardée BRUTE aussi : un
|
||||||
|
* lot renvoyé (`[…]`) n'est pas un objet, et QJsonDocument::object() le rendrait vide.
|
||||||
|
*/
|
||||||
|
class Sonde
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit Sonde(const QString &endpoint) : m_s(socketVers(endpoint, &m_proprietaire)) {}
|
||||||
|
|
||||||
|
bool ouverte() { return m_s && waitFor([this] { return connecte(m_s); }, 5000); }
|
||||||
|
|
||||||
|
void envoyer(const QByteArray &ligne)
|
||||||
|
{
|
||||||
|
m_s->write(ligne + '\n');
|
||||||
|
if (auto *l = qobject_cast<QLocalSocket *>(m_s))
|
||||||
|
l->flush();
|
||||||
|
else if (auto *t = qobject_cast<QTcpSocket *>(m_s))
|
||||||
|
t->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
//! La ligne suivante (sans son '\n') ; faux si rien n'arrive en \p ms, ou si la connexion se ferme avant.
|
||||||
|
bool recevoir(QByteArray *ligne, int ms = 5000)
|
||||||
|
{
|
||||||
|
waitFor([this] { lire(); return m_tampon.contains('\n') || !connecte(m_s); }, ms);
|
||||||
|
lire();
|
||||||
|
const int nl = m_tampon.indexOf('\n');
|
||||||
|
if (nl < 0)
|
||||||
|
return false;
|
||||||
|
*ligne = m_tampon.left(nl);
|
||||||
|
m_tampon.remove(0, nl + 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//! Vrai si l'AUTRE côté ferme en \p ms : la sonde ne ferme jamais d'elle-même.
|
||||||
|
bool fermeeParLautre(int ms) { return waitFor([this] { lire(); return !connecte(m_s); }, ms); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void lire() { m_tampon += m_s->readAll(); }
|
||||||
|
|
||||||
|
QObject m_proprietaire;
|
||||||
|
QIODevice *m_s;
|
||||||
|
QByteArray m_tampon;
|
||||||
|
};
|
||||||
|
|
||||||
|
//! Une réponse error de la sonde : `{"jsonrpc":"2.0","id":<id>,"error":{"code":<code>,…}}`, objet seul.
|
||||||
|
QString verifierErreur(const QByteArray &ligne, const QJsonValue &id, int code)
|
||||||
|
{
|
||||||
|
QJsonParseError e;
|
||||||
|
const QJsonDocument doc = QJsonDocument::fromJson(ligne, &e);
|
||||||
|
const QString vu = QString::fromUtf8(ligne.left(300));
|
||||||
|
if (e.error != QJsonParseError::NoError || !doc.isObject())
|
||||||
|
return QStringLiteral("réponse qui n'est pas UN objet JSON : ") + vu;
|
||||||
|
const QJsonObject m = doc.object();
|
||||||
|
if (!m.contains("error") || m.value("error").toObject().value("code").toInt() != code)
|
||||||
|
return QStringLiteral("erreur %1 attendue : ").arg(code) + vu;
|
||||||
|
const QJsonValue recu = m.value("id");
|
||||||
|
const bool bonId = id.isNull() ? m.contains("id") && recu.isNull() : recu.isDouble() && recu.toDouble() == id.toDouble();
|
||||||
|
if (!bonId)
|
||||||
|
return QStringLiteral("id %1 attendu : ").arg(id.isNull() ? QStringLiteral("null") : QString::number(id.toInt())) + vu;
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
QString cle(const QVariantMap &v)
|
QString cle(const QVariantMap &v)
|
||||||
{
|
{
|
||||||
return QString::fromUtf8(QJsonDocument::fromVariant(v).toJson(QJsonDocument::Compact));
|
return QString::fromUtf8(QJsonDocument::fromVariant(v).toJson(QJsonDocument::Compact));
|
||||||
@ -336,9 +404,23 @@ private slots:
|
|||||||
void testTheSessionMatchesTheContract();
|
void testTheSessionMatchesTheContract();
|
||||||
void testEveryPlanIsAccepted();
|
void testEveryPlanIsAccepted();
|
||||||
void testTheSafetyLoopStaysOnTop();
|
void testTheSafetyLoopStaysOnTop();
|
||||||
|
void testGetPlanBeforeTheHandshakeIsRefused();
|
||||||
|
void testARefusedHandshakeIsClosedByTheOptimizer();
|
||||||
|
void testABatchIsRefused();
|
||||||
|
void testALineOverTheLimitIsRefused();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void monter();
|
void monter();
|
||||||
|
//! Où l'optimiseur écoute ($ETM_POWERSYNC_OPTIMIZER_ENDPOINT, sinon un socket du test).
|
||||||
|
QString endpoint() const;
|
||||||
|
//! Le token que l'optimiseur attend ($ETM_POWERSYNC_OPTIMIZER_TOKEN_FILE), vide sinon.
|
||||||
|
QString lireToken();
|
||||||
|
//! Points 6 à 9 : l'optimiseur seul, sans moteur ; \p sonde ouverte dessus.
|
||||||
|
void sonder(std::unique_ptr<Sonde> &sonde);
|
||||||
|
//! Un Handshake de sonde en \p version, avec \p token s'il n'est pas vide.
|
||||||
|
QByteArray handshake(int id, const QString &version, const QString &token) const;
|
||||||
|
//! Handshake accepté sur \p sonde, ou échec du test.
|
||||||
|
void ouvrirSession(Sonde &sonde, int id);
|
||||||
//! Lance l'optimiseur et y branche le moteur (directement, ou par le relais).
|
//! Lance l'optimiseur et y branche le moteur (directement, ou par le relais).
|
||||||
void brancher(bool parLeRelais);
|
void brancher(bool parLeRelais);
|
||||||
void cycle(const QDateTime &t);
|
void cycle(const QDateTime &t);
|
||||||
@ -437,20 +519,32 @@ void TestOptimizerConformance::monter()
|
|||||||
m_arb->setOptimizerTraceDirectory(d);
|
m_arb->setOptimizerTraceDirectory(d);
|
||||||
QCOMPARE(rpc("SetOptimizerTrace", {{"enabled", true}}).value("energyError").toString(), QString("EnergyErrorNoError"));
|
QCOMPARE(rpc("SetOptimizerTrace", {{"enabled", true}}).value("energyError").toString(), QString("EnergyErrorNoError"));
|
||||||
// Token : celui que l'optimiseur attend, s'il en attend un.
|
// Token : celui que l'optimiseur attend, s'il en attend un.
|
||||||
const QString fichier = variable("ETM_POWERSYNC_OPTIMIZER_TOKEN_FILE");
|
m_token = lireToken();
|
||||||
if (!fichier.isEmpty()) {
|
if (QTest::currentTestFailed())
|
||||||
QFile f(fichier);
|
return;
|
||||||
QVERIFY2(f.open(QIODevice::ReadOnly), qUtf8Printable("ETM_POWERSYNC_OPTIMIZER_TOKEN_FILE illisible : " + fichier));
|
if (!m_token.isEmpty())
|
||||||
m_token = QString::fromUtf8(f.readAll().trimmed());
|
|
||||||
QCOMPARE(rpc("SetOptimizerToken", {{"token", m_token}}).value("energyError").toString(), QString("EnergyErrorNoError"));
|
QCOMPARE(rpc("SetOptimizerToken", {{"token", m_token}}).value("energyError").toString(), QString("EnergyErrorNoError"));
|
||||||
}
|
|
||||||
m_surplusW = 0;
|
m_surplusW = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString TestOptimizerConformance::endpoint() const
|
||||||
|
{
|
||||||
|
return variable("ETM_POWERSYNC_OPTIMIZER_ENDPOINT", QStringLiteral("unix://%1/optimiseur.sock").arg(m_dir.path()));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString TestOptimizerConformance::lireToken()
|
||||||
|
{
|
||||||
|
const QString fichier = variable("ETM_POWERSYNC_OPTIMIZER_TOKEN_FILE");
|
||||||
|
if (fichier.isEmpty())
|
||||||
|
return QString();
|
||||||
|
QFile f(fichier);
|
||||||
|
[&] { QVERIFY2(f.open(QIODevice::ReadOnly), qUtf8Printable("ETM_POWERSYNC_OPTIMIZER_TOKEN_FILE illisible : " + fichier)); }();
|
||||||
|
return QString::fromUtf8(f.readAll().trimmed());
|
||||||
|
}
|
||||||
|
|
||||||
void TestOptimizerConformance::brancher(bool parLeRelais)
|
void TestOptimizerConformance::brancher(bool parLeRelais)
|
||||||
{
|
{
|
||||||
const QString endpoint = variable("ETM_POWERSYNC_OPTIMIZER_ENDPOINT",
|
const QString endpoint = this->endpoint();
|
||||||
QStringLiteral("unix://%1/optimiseur.sock").arg(m_dir.path()));
|
|
||||||
QString erreur;
|
QString erreur;
|
||||||
QVERIFY2(m_optimiseur.demarrer(endpoint, &erreur), qUtf8Printable(erreur));
|
QVERIFY2(m_optimiseur.demarrer(endpoint, &erreur), qUtf8Printable(erreur));
|
||||||
QString cible = endpoint;
|
QString cible = endpoint;
|
||||||
@ -710,5 +804,136 @@ void TestOptimizerConformance::testTheSafetyLoopStaysOnTop()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 6 à 9 : protocole 1.2, par une connexion de sonde ----------------------------------------------
|
||||||
|
|
||||||
|
void TestOptimizerConformance::sonder(std::unique_ptr<Sonde> &sonde)
|
||||||
|
{
|
||||||
|
QVERIFY(m_dir.isValid());
|
||||||
|
m_token = lireToken();
|
||||||
|
if (QTest::currentTestFailed())
|
||||||
|
return;
|
||||||
|
QString erreur;
|
||||||
|
QVERIFY2(m_optimiseur.demarrer(endpoint(), &erreur), qUtf8Printable(erreur));
|
||||||
|
sonde.reset(new Sonde(endpoint()));
|
||||||
|
QVERIFY2(sonde->ouverte(), qUtf8Printable("la sonde ne se connecte pas à " + endpoint()));
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray TestOptimizerConformance::handshake(int id, const QString &version, const QString &token) const
|
||||||
|
{
|
||||||
|
QJsonObject params{{"protocolVersion", version}, {"engine", "etm-powersync-energy-plugin-etm/conformance"}};
|
||||||
|
if (!token.isEmpty())
|
||||||
|
params.insert("token", token);
|
||||||
|
return QJsonDocument(QJsonObject{{"jsonrpc", "2.0"}, {"id", id}, {"method", "Handshake"}, {"params", params}})
|
||||||
|
.toJson(QJsonDocument::Compact);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestOptimizerConformance::ouvrirSession(Sonde &sonde, int id)
|
||||||
|
{
|
||||||
|
sonde.envoyer(handshake(id, QString::fromLatin1(OptimizerProtocol::ProtocolVersion), m_token));
|
||||||
|
QByteArray ligne;
|
||||||
|
QVERIFY2(sonde.recevoir(&ligne), "handshake de sonde sans réponse");
|
||||||
|
const QJsonObject m = QJsonDocument::fromJson(ligne).object();
|
||||||
|
QVERIFY2(m.value("id").toInt() == id && m.value("result").toObject().value("protocolVersion").isString(),
|
||||||
|
qUtf8Printable("handshake de sonde refusé : " + QString::fromUtf8(ligne.left(300))));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestOptimizerConformance::testGetPlanBeforeTheHandshakeIsRefused()
|
||||||
|
{
|
||||||
|
// §3 (1.2) : GetPlan avant un handshake réussi → -32600, et la connexion est GARDÉE — le
|
||||||
|
// handshake qui suit, sur la même connexion, est accepté.
|
||||||
|
std::unique_ptr<Sonde> sonde;
|
||||||
|
sonder(sonde);
|
||||||
|
if (QTest::currentTestFailed())
|
||||||
|
return;
|
||||||
|
sonde->envoyer(R"({"jsonrpc":"2.0","id":1,"method":"GetPlan","params":{}})");
|
||||||
|
QByteArray ligne;
|
||||||
|
QVERIFY2(sonde->recevoir(&ligne), "GetPlan avant le handshake : aucune réponse (ou connexion fermée)");
|
||||||
|
const QString e = verifierErreur(ligne, 1, -32600);
|
||||||
|
QVERIFY2(e.isEmpty(), qUtf8Printable(e));
|
||||||
|
ouvrirSession(*sonde, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestOptimizerConformance::testARefusedHandshakeIsClosedByTheOptimizer()
|
||||||
|
{
|
||||||
|
// §3 (1.2) : un handshake refusé est fermé des deux côtés. Ici, la moitié de l'OPTIMISEUR : il
|
||||||
|
// ferme après avoir envoyé l'erreur (la sonde, elle, ne ferme pas). Majeure incompatible →
|
||||||
|
// -32602 ; si l'optimiseur attend un token, un token faux → -32001, fermé pareil. La moitié du
|
||||||
|
// moteur est éprouvée par la suite `optimizerclient` (testARefusedHandshakeIsClosedByTheEngine).
|
||||||
|
std::unique_ptr<Sonde> sonde;
|
||||||
|
sonder(sonde);
|
||||||
|
if (QTest::currentTestFailed())
|
||||||
|
return;
|
||||||
|
const QString autreMajeure = QString::number(QString::fromLatin1(OptimizerProtocol::ProtocolVersion).section('.', 0, 0).toInt() + 1) + ".0";
|
||||||
|
sonde->envoyer(handshake(1, autreMajeure, m_token));
|
||||||
|
QByteArray ligne;
|
||||||
|
QVERIFY2(sonde->recevoir(&ligne), qUtf8Printable("handshake en " + autreMajeure + " : aucune réponse avant la fermeture"));
|
||||||
|
QString e = verifierErreur(ligne, 1, -32602);
|
||||||
|
QVERIFY2(e.isEmpty(), qUtf8Printable(e));
|
||||||
|
QVERIFY2(sonde->fermeeParLautre(5000), "handshake refusé (-32602) : l'optimiseur n'a pas fermé la connexion en 5 s");
|
||||||
|
|
||||||
|
if (m_token.isEmpty()) {
|
||||||
|
qInfo().noquote() << "[Conformité] aucun token attendu : refus -32001 non éprouvé";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Sonde faux(endpoint());
|
||||||
|
QVERIFY(faux.ouverte());
|
||||||
|
faux.envoyer(handshake(1, QString::fromLatin1(OptimizerProtocol::ProtocolVersion), m_token + QStringLiteral("-faux")));
|
||||||
|
QVERIFY2(faux.recevoir(&ligne), "token faux : aucune réponse avant la fermeture");
|
||||||
|
e = verifierErreur(ligne, 1, -32001);
|
||||||
|
QVERIFY2(e.isEmpty(), qUtf8Printable(e));
|
||||||
|
QVERIFY2(faux.fermeeParLautre(5000), "handshake refusé (-32001) : l'optimiseur n'a pas fermé la connexion en 5 s");
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestOptimizerConformance::testABatchIsRefused()
|
||||||
|
{
|
||||||
|
// §2 (1.2) : lots interdits. Un lot reçu → UNE réponse -32600 à id null, aucun de ses messages
|
||||||
|
// traité, connexion gardée. Le lot porte une requête dont la réponse est normée (UpdateLearned
|
||||||
|
// avec id → -32601, §8) : la traiter se verrait.
|
||||||
|
std::unique_ptr<Sonde> sonde;
|
||||||
|
sonder(sonde);
|
||||||
|
if (QTest::currentTestFailed())
|
||||||
|
return;
|
||||||
|
ouvrirSession(*sonde, 1);
|
||||||
|
if (QTest::currentTestFailed())
|
||||||
|
return;
|
||||||
|
sonde->envoyer(R"([{"jsonrpc":"2.0","id":2,"method":"UpdateLearned","params":{}}])");
|
||||||
|
QByteArray ligne;
|
||||||
|
QVERIFY2(sonde->recevoir(&ligne), "lot : aucune réponse (ou connexion fermée)");
|
||||||
|
QString e = verifierErreur(ligne, QJsonValue::Null, -32600);
|
||||||
|
QVERIFY2(e.isEmpty(), qUtf8Printable(e));
|
||||||
|
// Connexion gardée, et la ligne suivante répond à la requête suivante — pas au contenu du lot.
|
||||||
|
sonde->envoyer(R"({"jsonrpc":"2.0","id":3,"method":"UpdateLearned","params":{}})");
|
||||||
|
QVERIFY2(sonde->recevoir(&ligne), "après le lot : aucune réponse (ou connexion fermée)");
|
||||||
|
e = verifierErreur(ligne, 3, -32601);
|
||||||
|
QVERIFY2(e.isEmpty(), qUtf8Printable(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
void TestOptimizerConformance::testALineOverTheLimitIsRefused()
|
||||||
|
{
|
||||||
|
// §2 (1.2) : maxLineBytes dans les deux sens, '\n' non compté. Un octet de trop → -32600 à id
|
||||||
|
// null, la ligne n'est pas traitée, connexion gardée ; une ligne d'exactement maxLineBytes est
|
||||||
|
// lue normalement.
|
||||||
|
std::unique_ptr<Sonde> sonde;
|
||||||
|
sonder(sonde);
|
||||||
|
if (QTest::currentTestFailed())
|
||||||
|
return;
|
||||||
|
ouvrirSession(*sonde, 1);
|
||||||
|
if (QTest::currentTestFailed())
|
||||||
|
return;
|
||||||
|
auto ligneDe = [](int id, int taille) {
|
||||||
|
QByteArray l = QStringLiteral(R"({"jsonrpc":"2.0","id":%1,"method":"UpdateLearned","params":{}})").arg(id).toUtf8();
|
||||||
|
return l + QByteArray(taille - l.size(), ' ');
|
||||||
|
};
|
||||||
|
sonde->envoyer(ligneDe(2, OptimizerClient::kMaxLineBytes + 1));
|
||||||
|
QByteArray ligne;
|
||||||
|
QVERIFY2(sonde->recevoir(&ligne, 10000), "ligne trop longue : aucune réponse (ou connexion fermée)");
|
||||||
|
QString e = verifierErreur(ligne, QJsonValue::Null, -32600);
|
||||||
|
QVERIFY2(e.isEmpty(), qUtf8Printable(e));
|
||||||
|
sonde->envoyer(ligneDe(3, OptimizerClient::kMaxLineBytes));
|
||||||
|
QVERIFY2(sonde->recevoir(&ligne, 10000), "ligne d'exactement maxLineBytes : aucune réponse (ou connexion fermée)");
|
||||||
|
e = verifierErreur(ligne, 3, -32601);
|
||||||
|
QVERIFY2(e.isEmpty(), qUtf8Printable(e));
|
||||||
|
}
|
||||||
|
|
||||||
QTEST_MAIN(TestOptimizerConformance)
|
QTEST_MAIN(TestOptimizerConformance)
|
||||||
#include "testconformance.moc"
|
#include "testconformance.moc"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user