Conformité : forme d'error.data et ordre token puis majeure (protocole 1.3)
Deux fonctions, par la connexion de sonde :
- testAnInvalidContextIsRefusedWithItsProblems : un GetPlan de contexte invalide
({"timestamp": "valeur-sonde:7Qz9"}) → -32602 à son id, connexion gardée ; la session de sonde
jugée par le validateur publié contre le contrat EMBARQUÉ (--responses-only) : forme exigée
d'un optimiseur qui s'annonce 1.3 ou plus, AVERTISSEMENT seulement pour un 1.2 ; le contrôle
doit avoir eu lieu (GetPlan.error.data compté) ; garde indépendante : la valeur reçue n'est
pas dans error.data, quelle que soit la version.
- testTheTokenIsCheckedBeforeTheMajor : token faux et autre majeure → -32001 puis fermeture ;
-32602 d'un optimiseur 1.2 averti ; SKIP sans token attendu.
Éprouvé contre l'optimiseur de référence : 1.3 conforme (11 verts + 1 SKIP sans token, 3/3 avec
token) ; old-data annoncé 1.3 refusé, annoncé 1.2 averti ; echo-value refusé en 1.3 comme en 1.2 ;
major-first refusé en 1.3, averti en 1.2 ; référence annoncée 1.2 verte sans avertissement.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d961b73ed7
commit
651181f84e
@ -15,11 +15,13 @@ adaptateurs, sur les mocks de la suite `simulation` — et vérifie, une fonctio
|
||||
| `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 |
|
||||
| `testAnInvalidContextIsRefusedWithItsProblems` | protocole 1.3, §7 : un `GetPlan` dont le contexte est invalide (`{"timestamp": "valeur-sonde:7Qz9"}`) → `-32602` à son `id`, connexion **gardée** ; `error.data` jugé par le validateur publié contre le contrat embarqué (`--responses-only`) : forme `{"problems":[{path, problem}]}` **exigée** d'un optimiseur qui s'annonce 1.3 ou plus, **avertie** (`AVERTISSEMENT`, test vert) d'un optimiseur 1.2 ; la valeur reçue recopiée dans `error.data` est **refusée** quelle que soit la version (règle de 1.2) |
|
||||
| `testTheTokenIsCheckedBeforeTheMajor` | protocole 1.3, §3 : token faux **et** autre majeure dans le même handshake → `-32001` (le token d'abord), puis l'optimiseur ferme. Exigé à partir de 1.3 ; `-32602` d'un optimiseur 1.2 est averti. **SKIP** sans `ETM_POWERSYNC_OPTIMIZER_TOKEN_FILE` : sans token attendu, l'ordre ne s'éprouve pas |
|
||||
| `testTheSessionMatchesTheContractOffUtc` | la fonction 3, le moteur sous un fuseau **non UTC** (`Europe/Berlin`) et cadencé en heure locale comme en production, à travers le passage à l'heure d'hiver (02:58 CEST → 02:01 CET) : aucun échec de requête, session conforme au contrat embarqué (tout instant porte son décalage, §5.2) |
|
||||
|
||||
Ces deux dernières lignes valent pour le mode `plan` (défaut). Voir **Optimiseur qui s'abstient**.
|
||||
Les lignes `testEveryPlanIsAccepted` et `testTheSafetyLoopStaysOnTop` 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
|
||||
Les fonctions 6 à 9, 11 et 12 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é.
|
||||
@ -42,7 +44,8 @@ export ETM_POWERSYNC_OPTIMIZER_CMD='exec /chemin/de/mon-optimiseur --listen {end
|
||||
for fn in testHandshakeIsAccepted testGetPlanIsAnsweredInTime testTheSessionMatchesTheContract \
|
||||
testEveryPlanIsAccepted testTheSafetyLoopStaysOnTop testGetPlanBeforeTheHandshakeIsRefused \
|
||||
testARefusedHandshakeIsClosedByTheOptimizer testABatchIsRefused testALineOverTheLimitIsRefused \
|
||||
testTheSessionMatchesTheContractOffUtc; do
|
||||
testTheSessionMatchesTheContractOffUtc testAnInvalidContextIsRefusedWithItsProblems \
|
||||
testTheTokenIsCheckedBeforeTheMajor; do
|
||||
systemd-run --user --scope -q -p MemoryMax=2G \
|
||||
bwrap --dev-bind / / --unshare-net --tmpfs /tmp --bind "$PWD/../../.." "$PWD/../../.." \
|
||||
--bind <sources> <sources> --chdir "$PWD" ./target_wrapper.sh ./etm-powersync-optimizer-conformance "$fn"
|
||||
@ -61,6 +64,19 @@ commande vit dans le même réseau privé (la boucle locale y fonctionne, `tcp:/
|
||||
| `ETM_POWERSYNC_SOURCE_DIR` | sources du build | Racine où trouver le validateur et l'optimiseur de référence |
|
||||
| `ETM_POWERSYNC_CONFORMANCE_EXPECT` | `plan` | Ce que la suite attend : `plan` (l'optimiseur commande) ou `abstain` (il s'abstient à chaque cycle). Toute autre valeur fait échouer |
|
||||
|
||||
**Un optimiseur 1.2 face aux règles 1.3.** Une mineure ajoute des règles (protocole §9) : la suite
|
||||
lit la version que l'optimiseur annonce au handshake et n'exige la forme d'`error.data` et l'ordre
|
||||
token puis majeure qu'à partir de 1.3. En deçà, l'écart est écrit en `AVERTISSEMENT` et la fonction
|
||||
reste verte — l'optimiseur fonctionne avec ce moteur, il s'aligne avant de s'annoncer 1.3. Pour
|
||||
éprouver l'ordre, donner un token (`ETM_POWERSYNC_OPTIMIZER_TOKEN_FILE`, et le même fichier à
|
||||
l'optimiseur) :
|
||||
|
||||
```sh
|
||||
printf 'jeton-de-conformite-0123456789' > /chemin/token
|
||||
export ETM_POWERSYNC_OPTIMIZER_TOKEN_FILE=/chemin/token
|
||||
export ETM_POWERSYNC_OPTIMIZER_CMD='exec /chemin/de/mon-optimiseur --listen {endpoint} --token-file /chemin/token'
|
||||
```
|
||||
|
||||
L'attente de démarrage ouvre une connexion de sonde, refermée sans handshake : un serveur conforme
|
||||
la tolère. Un échec dit ce qui manque — code d'erreur du client, écart au contrat (chemin, mot-clé),
|
||||
cycles non appliqués avec leur `fallbackCode`, latences.
|
||||
@ -95,7 +111,7 @@ fondée sur ces champs jugerait faux dans les deux cas : le mode se donne, il ne
|
||||
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` émis par l'optimiseur (ignoré par le moteur ; sa
|
||||
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`
|
||||
le moteur n'envoie de token que si l'installateur en a posé un), l'exhaustivité de la validation du
|
||||
contexte par l'optimiseur (un seul contexte invalide est envoyé ; il en suffit d'un problème pour
|
||||
juger la forme), le comportement sous défaut de transport. Le contrat : `docs/etm-powersync-optimizer-protocol.openrpc.json`
|
||||
(prose : `docs/OPTIMIZER_PROTOCOL.md`, annexe G).
|
||||
|
||||
@ -22,7 +22,11 @@
|
||||
// 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) ;
|
||||
// 10. le point 3, le moteur réglé sur un fuseau NON UTC (Europe/Berlin) et cadencé en heure
|
||||
// locale comme en production, à travers le passage à l'heure d'hiver.
|
||||
// locale comme en production, à travers le passage à l'heure d'hiver ;
|
||||
// 11 et 12 (protocole 1.3), par la sonde : un contexte GetPlan invalide → -32602 dont `error.data`
|
||||
// a la forme normée, jugée par le validateur publié — exigée d'un optimiseur qui s'annonce 1.3
|
||||
// ou plus, avertie pour un 1.2 ; la valeur reçue n'y est jamais recopiée. Token faux ET autre
|
||||
// majeure dans le même handshake → -32001 (le token d'abord), puis fermeture.
|
||||
//
|
||||
// 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
|
||||
@ -420,6 +424,8 @@ private slots:
|
||||
void testABatchIsRefused();
|
||||
void testALineOverTheLimitIsRefused();
|
||||
void testTheSessionMatchesTheContractOffUtc();
|
||||
void testAnInvalidContextIsRefusedWithItsProblems();
|
||||
void testTheTokenIsCheckedBeforeTheMajor();
|
||||
|
||||
private:
|
||||
void monter();
|
||||
@ -431,8 +437,8 @@ private:
|
||||
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);
|
||||
//! Handshake accepté sur \p sonde, ou échec du test ; \p reponse : le `result` reçu, si demandé.
|
||||
void ouvrirSession(Sonde &sonde, int id, QJsonObject *reponse = nullptr);
|
||||
//! Lance l'optimiseur et y branche le moteur (directement, ou par le relais).
|
||||
void brancher(bool parLeRelais);
|
||||
void cycle(const QDateTime &t);
|
||||
@ -846,7 +852,7 @@ QByteArray TestOptimizerConformance::handshake(int id, const QString &version, c
|
||||
.toJson(QJsonDocument::Compact);
|
||||
}
|
||||
|
||||
void TestOptimizerConformance::ouvrirSession(Sonde &sonde, int id)
|
||||
void TestOptimizerConformance::ouvrirSession(Sonde &sonde, int id, QJsonObject *reponse)
|
||||
{
|
||||
sonde.envoyer(handshake(id, QString::fromLatin1(OptimizerProtocol::ProtocolVersion), m_token));
|
||||
QByteArray ligne;
|
||||
@ -854,8 +860,33 @@ void TestOptimizerConformance::ouvrirSession(Sonde &sonde, int id)
|
||||
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))));
|
||||
if (reponse)
|
||||
*reponse = m;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
//! « 1.3 » → 103 ; illisible → -1. Seules majeure et mineure comptent (§9).
|
||||
int versionComparable(const QString &v)
|
||||
{
|
||||
bool okMajeure = false, okMineure = true;
|
||||
const int majeure = v.section('.', 0, 0).toInt(&okMajeure);
|
||||
const int mineure = v.contains('.') ? v.section('.', 1, 1).toInt(&okMineure) : 0;
|
||||
return okMajeure && okMineure ? majeure * 100 + mineure : -1;
|
||||
}
|
||||
|
||||
//! Version à partir de laquelle les règles 1.3 sont EXIGÉES ; en deçà, un écart est averti.
|
||||
constexpr int kExigeDepuis = 103;
|
||||
|
||||
//! Vrai si \p annoncee est LISIBLE et antérieure à 1.3 — une version illisible est jugée 1.3.
|
||||
bool avantRegles13(const QString &annoncee)
|
||||
{
|
||||
const int v = versionComparable(annoncee);
|
||||
return v >= 0 && v < kExigeDepuis;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void TestOptimizerConformance::testGetPlanBeforeTheHandshakeIsRefused()
|
||||
{
|
||||
// §3 (1.2) : GetPlan avant un handshake réussi → -32600, et la connexion est GARDÉE — le
|
||||
@ -983,5 +1014,111 @@ void TestOptimizerConformance::testTheSessionMatchesTheContractOffUtc()
|
||||
jugerLaSession();
|
||||
}
|
||||
|
||||
// --- 11 et 12 : protocole 1.3, par une connexion de sonde --------------------------------------------
|
||||
|
||||
void TestOptimizerConformance::testAnInvalidContextIsRefusedWithItsProblems()
|
||||
{
|
||||
// §7 (1.2) : un contexte GetPlan invalide → -32602, connexion gardée. §7 (1.3) : `error.data`
|
||||
// = {"problems":[{path, problem}]}, jamais la valeur reçue. La forme est jugée par le VALIDATEUR
|
||||
// PUBLIÉ, sur la session de sonde (--responses-only : la requête est fautive à dessein) et
|
||||
// contre le contrat EMBARQUÉ ; c'est lui qui exige ou avertit selon la version que
|
||||
// l'optimiseur a annoncée. Le contexte porte une valeur qu'aucun chemin conforme ne peut
|
||||
// contenir : si elle revient dans `error.data`, c'est une recopie.
|
||||
static const QString marqueur = QStringLiteral("valeur-sonde:7Qz9");
|
||||
std::unique_ptr<Sonde> sonde;
|
||||
sonder(sonde);
|
||||
if (QTest::currentTestFailed())
|
||||
return;
|
||||
QJsonObject hsReponse;
|
||||
ouvrirSession(*sonde, 1, &hsReponse);
|
||||
if (QTest::currentTestFailed())
|
||||
return;
|
||||
const QString annoncee = hsReponse.value("result").toObject().value("protocolVersion").toString();
|
||||
const QJsonObject getPlan{{"jsonrpc", "2.0"}, {"id", 2}, {"method", "GetPlan"},
|
||||
{"params", QJsonObject{{"timestamp", marqueur}}}};
|
||||
sonde->envoyer(QJsonDocument(getPlan).toJson(QJsonDocument::Compact));
|
||||
QByteArray ligne;
|
||||
QVERIFY2(sonde->recevoir(&ligne), "contexte invalide : aucune réponse (ou connexion fermée)");
|
||||
const QString e = verifierErreur(ligne, 2, -32602);
|
||||
QVERIFY2(e.isEmpty(), qUtf8Printable(e));
|
||||
const QJsonObject reponse = QJsonDocument::fromJson(ligne).object();
|
||||
// Garde indépendante du validateur : la valeur reçue n'est nulle part dans `error.data`.
|
||||
const QJsonValue data = reponse.value("error").toObject().value("data");
|
||||
const QByteArray dataTexte = QJsonDocument(QJsonArray{data}).toJson(QJsonDocument::Compact);
|
||||
QVERIFY2(!dataTexte.contains(marqueur.toUtf8()),
|
||||
"error.data recopie la valeur reçue (params.timestamp) — jamais la valeur reçue (§7)");
|
||||
|
||||
// La session de sonde, jugée par le validateur publié. Le token y devient des astérisques.
|
||||
QJsonObject hsParams = QJsonDocument::fromJson(handshake(1, QString::fromLatin1(OptimizerProtocol::ProtocolVersion),
|
||||
m_token)).object();
|
||||
if (!m_token.isEmpty()) {
|
||||
QJsonObject p = hsParams.value("params").toObject();
|
||||
p.insert("token", QString(m_token.size(), QLatin1Char('*')));
|
||||
hsParams.insert("params", p);
|
||||
}
|
||||
QTemporaryDir dir;
|
||||
QVERIFY(dir.isValid());
|
||||
const Contrat::Verdict v = Contrat::juger({hsParams, hsReponse, getPlan, reponse},
|
||||
Contrat::ecrireSchema(dir, OptimizerProtocol::schemaDocumentBytes()),
|
||||
{QStringLiteral("--responses-only")});
|
||||
QVERIFY2(v.code == 0, qUtf8Printable(QStringLiteral("optimiseur en %1 — error.data NON CONFORME au contrat :\n")
|
||||
.arg(annoncee.left(16)) + v.sortie.right(4000)));
|
||||
// Le contrôle a bien eu lieu : un validateur qui ne trouverait plus la forme dans le contrat
|
||||
// passerait au vert sans rien juger.
|
||||
QVERIFY2(v.compte("GetPlan.error.data") == 1, qUtf8Printable(v.sortie.right(1000)));
|
||||
const int avertissements = v.resume.value("avertissements").toInt();
|
||||
for (const QString &l : v.sortie.split('\n'))
|
||||
if (l.startsWith(QStringLiteral("AVERTISSEMENT")))
|
||||
qWarning().noquote() << "[Conformité]" << l;
|
||||
// Un avertissement n'est permis qu'à un optimiseur d'avant 1.3 : au-delà, c'est un écart.
|
||||
QVERIFY2(avertissements == 0 || avantRegles13(annoncee), qUtf8Printable(v.sortie.right(1000)));
|
||||
qInfo().noquote() << "[Conformité] contexte invalide : -32602, optimiseur en" << annoncee
|
||||
<< (avertissements ? QStringLiteral("— forme d'error.data AVERTIE (%1), à aligner avant 1.3").arg(avertissements)
|
||||
: QStringLiteral("— forme d'error.data conforme"));
|
||||
// Connexion gardée : la sonde peut encore parler.
|
||||
sonde->envoyer(R"({"jsonrpc":"2.0","id":3,"method":"UpdateLearned","params":{}})");
|
||||
QVERIFY2(sonde->recevoir(&ligne), "après le contexte invalide : aucune réponse (ou connexion fermée)");
|
||||
const QString e2 = verifierErreur(ligne, 3, -32601);
|
||||
QVERIFY2(e2.isEmpty(), qUtf8Printable(e2));
|
||||
}
|
||||
|
||||
void TestOptimizerConformance::testTheTokenIsCheckedBeforeTheMajor()
|
||||
{
|
||||
// §3 (1.3) : token faux ET autre majeure dans le même handshake → -32001, le token d'abord,
|
||||
// puis l'optimiseur ferme. Exigé d'un optimiseur qui s'annonce 1.3 ou plus ; un optimiseur 1.2
|
||||
// qui juge la majeure d'abord (-32602) est averti. Sans token attendu, rien à éprouver.
|
||||
std::unique_ptr<Sonde> sonde;
|
||||
sonder(sonde);
|
||||
if (QTest::currentTestFailed())
|
||||
return;
|
||||
if (m_token.isEmpty())
|
||||
QSKIP("aucun token attendu (ETM_POWERSYNC_OPTIMIZER_TOKEN_FILE) : ordre token puis majeure non éprouvé");
|
||||
QJsonObject hsReponse;
|
||||
ouvrirSession(*sonde, 1, &hsReponse); // la version annoncée, sur une session acceptée
|
||||
if (QTest::currentTestFailed())
|
||||
return;
|
||||
const QString annoncee = hsReponse.value("result").toObject().value("protocolVersion").toString();
|
||||
const QString autreMajeure = QString::number(QString::fromLatin1(OptimizerProtocol::ProtocolVersion).section('.', 0, 0).toInt() + 1) + ".0";
|
||||
Sonde faux(endpoint());
|
||||
QVERIFY(faux.ouverte());
|
||||
faux.envoyer(handshake(1, autreMajeure, m_token + QStringLiteral("-faux")));
|
||||
QByteArray ligne;
|
||||
QVERIFY2(faux.recevoir(&ligne), "token faux et autre majeure : aucune réponse avant la fermeture");
|
||||
const QString e = verifierErreur(ligne, 1, -32001);
|
||||
if (!e.isEmpty()) {
|
||||
// La majeure jugée d'abord : -32602 (règle 1.2), un écart d'ordre seulement.
|
||||
const bool majeureDabord = verifierErreur(ligne, 1, -32602).isEmpty();
|
||||
QVERIFY2(majeureDabord && avantRegles13(annoncee),
|
||||
qUtf8Printable(QStringLiteral("optimiseur en %1 : ").arg(annoncee.left(16)) + e));
|
||||
qWarning().noquote() << "[Conformité] AVERTISSEMENT optimiseur en" << annoncee.left(16)
|
||||
<< ": token faux et autre majeure → -32602, la majeure jugée avant le token —"
|
||||
" -32001 exigé à partir de 1.3, à aligner avant de s'annoncer 1.3";
|
||||
} else {
|
||||
qInfo().noquote() << "[Conformité] token faux et autre majeure : -32001, le token d'abord (optimiseur en"
|
||||
<< annoncee.left(16) + ")";
|
||||
}
|
||||
QVERIFY2(faux.fermeeParLautre(5000), "handshake refusé : l'optimiseur n'a pas fermé la connexion en 5 s");
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestOptimizerConformance)
|
||||
#include "testconformance.moc"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user