test: les requêtes réelles du moteur jugées contre le contrat
optimizerclient : chaque Handshake envoyé ; socketscheduler : tout ce que le moteur envoie au faux optimiseur, plus testTheEngineSpeaksTheContractItServes (GetOptimizerProtocol == embarqué == docs, requêtes jugées avec le contrat servi) ; refoptimizer : session capturée par la référence, requêtes toujours, session entière et règles sémantiques en normal et en 1.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d95a65cb71
commit
07cb44d35f
@ -130,6 +130,13 @@ public:
|
||||
|
||||
int connections = 0;
|
||||
int disconnections = 0;
|
||||
//! Tout ce que le MOTEUR a envoyé à un faux optimiseur dans ce processus, dans l'ordre — jugé
|
||||
//! contre le contrat OpenRPC par la suite (lot « contrat OpenRPC »), puis vidé.
|
||||
static QList<QJsonObject> &capture()
|
||||
{
|
||||
static QList<QJsonObject> messages;
|
||||
return messages;
|
||||
}
|
||||
//! Répondeur automatique (brief 4 T3) : appelé sur chaque message reçu ; s'il rend vrai, le
|
||||
//! message est consommé et n'entre pas dans la file de waitMessage(). Vide = tout est filé.
|
||||
std::function<bool(const QJsonObject &)> responder;
|
||||
@ -146,6 +153,7 @@ private:
|
||||
while ((nl = m_buffer.indexOf('\n')) >= 0) {
|
||||
const QJsonObject message = QJsonDocument::fromJson(m_buffer.left(nl)).object();
|
||||
m_buffer.remove(0, nl + 1);
|
||||
capture().append(message);
|
||||
if (!(responder && responder(message)))
|
||||
m_messages.append(message);
|
||||
}
|
||||
|
||||
@ -6,6 +6,10 @@ PKGCONFIG += nymea
|
||||
|
||||
TARGET = etm-powersync-optimizerclient
|
||||
|
||||
# Lot contrat OpenRPC : les Handshake envoyés sont jugés par tools/etm-powersync-schema-check contre
|
||||
# docs/etm-powersync-optimizer-protocol.openrpc.json. Surchargée par $ETM_POWERSYNC_SOURCE_DIR.
|
||||
DEFINES += ETM_POWERSYNC_SOURCE_DIR=\\\"$$clean_path($$PWD/../../..)\\\"
|
||||
|
||||
# Cible VOLONTAIREMENT légère, comme loadmodel : aucun serveur nymea, aucun EnergyTestBase, et
|
||||
# du moteur le seul client de transport (brief 4 T2). Les faux optimiseurs sont des
|
||||
# QLocalServer/QTcpServer du processus de test, et le temps est SIMULÉ : aucun des délais
|
||||
@ -15,7 +19,8 @@ INCLUDEPATH += $$PWD/../../../energyplugin
|
||||
|
||||
HEADERS += $$PWD/../../../energyplugin/etm/scheduler/optimizerclient.h \
|
||||
$$PWD/../../../energyplugin/etm/scheduler/optimizersecret.h \
|
||||
$$PWD/../common/etm-powersync-fakeoptimizer.h
|
||||
$$PWD/../common/etm-powersync-fakeoptimizer.h \
|
||||
$$PWD/../common/etm-powersync-contrat.h
|
||||
SOURCES += $$PWD/../../../energyplugin/etm/scheduler/optimizerclient.cpp \
|
||||
$$PWD/../../../energyplugin/etm/scheduler/optimizersecret.cpp \
|
||||
testoptimizerclient.cpp
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
|
||||
#include "etm/scheduler/optimizerclient.h"
|
||||
#include "../common/etm-powersync-fakeoptimizer.h"
|
||||
#include "../common/etm-powersync-contrat.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
@ -88,6 +89,7 @@ class TestOptimizerClient : public QObject
|
||||
|
||||
private slots:
|
||||
void initTestCase();
|
||||
void cleanupTestCase();
|
||||
void init();
|
||||
|
||||
void testEndpointParsing_data();
|
||||
@ -118,6 +120,21 @@ private:
|
||||
QTemporaryDir m_dir;
|
||||
};
|
||||
|
||||
void TestOptimizerClient::cleanupTestCase()
|
||||
{
|
||||
// Lot contrat OpenRPC : chaque Handshake que le client a ENVOYÉ dans ce processus est jugé contre
|
||||
// le contrat (docs/ — cette suite n'embarque pas le sérialiseur). Les GetPlan de cette suite ne
|
||||
// le sont pas : leurs params sont FABRIQUÉS par le test, le client n'en connaît pas le contenu ;
|
||||
// ceux du sérialiseur réel sont jugés par `socketscheduler`, `refoptimizer` et `protocolschema`.
|
||||
const QList<QJsonObject> handshakes = Contrat::deMethode(FakeOptimizer::capture(), QStringLiteral("Handshake"));
|
||||
FakeOptimizer::capture().clear();
|
||||
if (handshakes.isEmpty())
|
||||
return;
|
||||
const Contrat::Verdict v = Contrat::juger(handshakes, Contrat::schemaDuDepot(), {QStringLiteral("--requests-only")});
|
||||
QVERIFY2(v.code == 0 && v.compte(QStringLiteral("Handshake.params")) == handshakes.size(),
|
||||
qUtf8Printable(QStringLiteral("Handshake du client NON CONFORME au contrat :\n") + v.sortie.right(3000)));
|
||||
}
|
||||
|
||||
void TestOptimizerClient::initTestCase()
|
||||
{
|
||||
QLoggingCategory::setFilterRules(QStringLiteral("NymeaEnergy.debug=true"));
|
||||
|
||||
@ -29,7 +29,8 @@ HEADERS += \
|
||||
$$SIM/experience/energylogger.h \
|
||||
$$SIM/experience/energymanagermock.h \
|
||||
$$SIM/simulationbase.h \
|
||||
$$PWD/../common/etm-powersync-fakeoptimizer.h
|
||||
$$PWD/../common/etm-powersync-fakeoptimizer.h \
|
||||
$$PWD/../common/etm-powersync-contrat.h
|
||||
|
||||
SOURCES += \
|
||||
$$SIM/experience/energyexperienceenergymock.cpp \
|
||||
|
||||
@ -23,6 +23,7 @@
|
||||
#include "simulationbase.h"
|
||||
#include "experience/energymanagermock.h"
|
||||
#include "../common/etm-powersync-fakeoptimizer.h"
|
||||
#include "../common/etm-powersync-contrat.h"
|
||||
|
||||
#include "energysettings.h"
|
||||
#include "etm/energyarbitrator.h"
|
||||
@ -223,8 +224,13 @@ private slots:
|
||||
void testTracesReplayWithZeroDivergence();
|
||||
void testSafetyWinsOverTheOptimizer();
|
||||
void testAnEchoedTokenLeaksNowhereEndToEnd();
|
||||
void cleanupTestCase();
|
||||
|
||||
private:
|
||||
//! Lot contrat OpenRPC : la session capturée par l'optimiseur de référence (\c --capture),
|
||||
//! jugée contre le contrat EMBARQUÉ ; \p complete = réponses comprises, règles sémantiques
|
||||
//! appliquées (modes « bons » seulement). La capture est consommée.
|
||||
Contrat::Verdict jugerLaSession(bool complete);
|
||||
void monter(Banc banc);
|
||||
//! Démarre \p ref et y pointe le moteur par la RPC de production ; attend la connexion si
|
||||
//! \p attendrePret.
|
||||
@ -345,7 +351,9 @@ void TestRefOptimizer::brancher(RefOptimizer &ref, Transport transport, const QS
|
||||
? QStringLiteral("unix://%1/ref-%2.sock").arg(m_dir.path()).arg(++n)
|
||||
: QStringLiteral("tcp://127.0.0.1:0");
|
||||
QString erreur;
|
||||
QVERIFY2(ref.demarrer(ecoute, args, &erreur), qUtf8Printable(erreur));
|
||||
// Toute session du MOTEUR avec la référence est capturée, puis jugée contre le contrat.
|
||||
QVERIFY2(ref.demarrer(ecoute, args + QStringList{QStringLiteral("--capture"), m_dir.path() + QStringLiteral("/capture.ndjson")},
|
||||
&erreur), qUtf8Printable(erreur));
|
||||
QCOMPARE(rpc("SetOptimizerEndpoint", {{"endpoint", ref.endpoint()}}).value("energyError").toString(),
|
||||
QString("EnergyErrorNoError"));
|
||||
SocketScheduler *s = m_arb->socketScheduler();
|
||||
@ -597,6 +605,13 @@ void TestRefOptimizer::testNormalReproducesTheRules()
|
||||
.arg(i).arg(surplus.at(i - 1)).arg(cleEtat(b.at(i))).arg(i - 1).arg(cleEtat(a.at(i - 1)));
|
||||
}
|
||||
QVERIFY2(ecarts.isEmpty(), qUtf8Printable(ecarts.join('\n')));
|
||||
// Lot contrat OpenRPC : la session ENTIÈRE est conforme — requêtes du moteur, réponses de la
|
||||
// référence, et chaque plan applicable selon les règles sémantiques du contrat.
|
||||
ref.arreter();
|
||||
const Contrat::Verdict v = jugerLaSession(true);
|
||||
QVERIFY2(v.code == 0, qUtf8Printable(v.sortie.right(4000)));
|
||||
QVERIFY2(v.compte("GetPlan.result") >= surplus.count() - 1 && v.compte("Handshake.result") >= 1,
|
||||
qUtf8Printable(v.sortie.right(1000)));
|
||||
// L'exemple GPL s'annonce `powersync-optimizer` ; `etm-optimizer` est l'optimiseur propriétaire.
|
||||
QCOMPARE(rpc("GetOptimizerStatus").value("name").toString(), QString("powersync-optimizer"));
|
||||
// Régime stable : à la fin de chaque palier, les deux runs coïncident aussi SANS décalage.
|
||||
@ -772,6 +787,10 @@ void TestRefOptimizer::testAProtocol10OptimizerIsAccepted()
|
||||
QCOMPARE(v.toMap().value("decision").toMap().value("code").toString(), QString(DecisionCode::OptimizerDecision));
|
||||
const QVariantMap plan = rpc("GetPlan");
|
||||
QCOMPARE(plan.value("strategy").toString(), QString("optimizer"));
|
||||
// Lot contrat OpenRPC : un optimiseur 1.0 est conforme au contrat 1.1 (champs 1.1 optionnels).
|
||||
ref.arreter();
|
||||
const Contrat::Verdict v = jugerLaSession(true);
|
||||
QVERIFY2(v.code == 0 && v.compte("GetPlan.result") >= 2, qUtf8Printable(v.sortie.right(4000)));
|
||||
}
|
||||
|
||||
void TestRefOptimizer::testTracesReplayWithZeroDivergence()
|
||||
@ -970,4 +989,33 @@ void TestRefOptimizer::testAnEchoedTokenLeaksNowhereEndToEnd()
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestRefOptimizer)
|
||||
// --- Lot contrat OpenRPC ----------------------------------------------------------------------
|
||||
|
||||
void TestRefOptimizer::cleanupTestCase()
|
||||
{
|
||||
SimulationBase::cleanupTestCase();
|
||||
// Tout ce que le MOTEUR a envoyé à la référence, quels que soient les modes : ses requêtes sont
|
||||
// toujours conformes, même quand la référence est réglée pour mal répondre.
|
||||
const Contrat::Verdict v = jugerLaSession(false);
|
||||
QVERIFY2(v.code == 0 || (v.code == -1 && v.sortie.isEmpty()), qUtf8Printable(QStringLiteral("requêtes du moteur NON CONFORMES au contrat :\n") + v.sortie.right(4000)));
|
||||
}
|
||||
|
||||
Contrat::Verdict TestRefOptimizer::jugerLaSession(bool complete)
|
||||
{
|
||||
QFile f(m_dir.path() + QStringLiteral("/capture.ndjson"));
|
||||
QList<QJsonObject> messages;
|
||||
if (f.open(QIODevice::ReadOnly)) {
|
||||
for (const QByteArray &ligne : f.readAll().split('\n'))
|
||||
if (!ligne.trimmed().isEmpty())
|
||||
messages << QJsonDocument::fromJson(ligne).object();
|
||||
f.close();
|
||||
f.remove();
|
||||
}
|
||||
if (messages.isEmpty())
|
||||
return Contrat::Verdict(); // code -1 : rien à juger
|
||||
QTemporaryDir dir;
|
||||
return Contrat::juger(messages, Contrat::ecrireSchema(dir, OptimizerProtocol::schemaDocumentBytes()),
|
||||
complete ? QStringList{QStringLiteral("--semantic")} : QStringList{QStringLiteral("--requests-only")});
|
||||
}
|
||||
|
||||
#include "testrefoptimizer.moc"
|
||||
|
||||
@ -15,6 +15,8 @@ CONFIG += testcase
|
||||
TARGET = etm-powersync-socketscheduler
|
||||
|
||||
DEFINES += ENERGY_SIMULATION
|
||||
# Lot contrat OpenRPC : le validateur et le contrat de docs/. Surchargée par $ETM_POWERSYNC_SOURCE_DIR.
|
||||
DEFINES += ETM_POWERSYNC_SOURCE_DIR=\\\"$$clean_path($$PWD/../../..)\\\"
|
||||
|
||||
SIM = $$PWD/../simulation
|
||||
INCLUDEPATH += $$SIM
|
||||
@ -28,7 +30,8 @@ HEADERS += \
|
||||
$$SIM/experience/energylogger.h \
|
||||
$$SIM/experience/energymanagermock.h \
|
||||
$$SIM/simulationbase.h \
|
||||
$$PWD/../common/etm-powersync-fakeoptimizer.h
|
||||
$$PWD/../common/etm-powersync-fakeoptimizer.h \
|
||||
$$PWD/../common/etm-powersync-contrat.h
|
||||
|
||||
SOURCES += \
|
||||
$$SIM/experience/energyexperienceenergymock.cpp \
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
#include "simulationbase.h"
|
||||
#include "experience/energymanagermock.h"
|
||||
#include "../common/etm-powersync-fakeoptimizer.h"
|
||||
#include "../common/etm-powersync-contrat.h"
|
||||
|
||||
#include "energysettings.h"
|
||||
#include "etm/energyarbitrator.h"
|
||||
@ -16,6 +17,7 @@
|
||||
#include "etm/scheduler/optimizerstatus.h"
|
||||
#include "etm/scheduler/publishedplan.h"
|
||||
#include "etm/scheduler/optimizertrace.h"
|
||||
#include "etm/scheduler/optimizerprotocol.h"
|
||||
#include "etm/types/decisionreason.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
@ -137,8 +139,13 @@ private slots:
|
||||
void testTracerPairsTheResponseByRequestId();
|
||||
// Brief 4 T7 — correctif A : un token recopié par l'optimiseur ne sort nulle part
|
||||
void testAnEchoedTokenLeaksNowhere();
|
||||
// Lot contrat OpenRPC — le moteur parle le contrat qu'il sert
|
||||
void testTheEngineSpeaksTheContractItServes();
|
||||
void cleanupTestCase();
|
||||
|
||||
private:
|
||||
//! Juge contre le contrat EMBARQUÉ tout ce que le moteur a envoyé au faux optimiseur, puis vide.
|
||||
void jugerLesRequetesDuMoteur();
|
||||
//! Banc commun ; \p avecOptimiseur faux = rien n'est configuré.
|
||||
void monter(bool avecOptimiseur = true, FakeOptimizer::Transport transport = FakeOptimizer::Unix);
|
||||
//! Un cycle à \p t, compteur à \p compteurW (négatif = export) ; attend le jugement de la
|
||||
@ -2120,4 +2127,65 @@ void TestSocketScheduler::testAnEchoedTokenLeaksNowhere()
|
||||
}
|
||||
|
||||
QTEST_MAIN(TestSocketScheduler)
|
||||
// --- Lot contrat OpenRPC ----------------------------------------------------------------------
|
||||
|
||||
void TestSocketScheduler::cleanupTestCase()
|
||||
{
|
||||
SimulationBase::cleanupTestCase();
|
||||
jugerLesRequetesDuMoteur();
|
||||
}
|
||||
|
||||
void TestSocketScheduler::jugerLesRequetesDuMoteur()
|
||||
{
|
||||
// TOUT ce que le moteur a émis dans ce processus — Handshake du client, GetPlan du sérialiseur
|
||||
// réel sur le contexte réel de l'arbitre —, jugé par le validateur publié contre le contrat
|
||||
// EMBARQUÉ. Appelé aussi par monter() (qui relance le serveur) : chaque banc est jugé.
|
||||
const QList<QJsonObject> requetes = FakeOptimizer::capture();
|
||||
FakeOptimizer::capture().clear();
|
||||
if (requetes.isEmpty())
|
||||
return;
|
||||
QTemporaryDir dir;
|
||||
QVERIFY(dir.isValid());
|
||||
const Contrat::Verdict v = Contrat::juger(requetes, Contrat::ecrireSchema(dir, OptimizerProtocol::schemaDocumentBytes()),
|
||||
{QStringLiteral("--requests-only")});
|
||||
QVERIFY2(v.code == 0, qUtf8Printable(QStringLiteral("requêtes du moteur NON CONFORMES au contrat embarqué :\n")
|
||||
+ v.sortie.right(4000)));
|
||||
}
|
||||
|
||||
void TestSocketScheduler::testTheEngineSpeaksTheContractItServes()
|
||||
{
|
||||
// Le contrat SERVI par NymeaEnergy.GetOptimizerProtocol est celui qu'embarque le binaire, et
|
||||
// celui de docs/ ; et ce que le moteur ENVOIE à un optimiseur y est conforme. Garde de
|
||||
// pertinence : des Handshake et des GetPlan ont réellement été jugés — un jugement sur une
|
||||
// capture vide ne dirait rien.
|
||||
FakeOptimizer::capture().clear();
|
||||
monter();
|
||||
m_repondre = planValide();
|
||||
const QDateTime t0 = utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0));
|
||||
for (int i = 0; i < 3; ++i)
|
||||
cycle(t0.addSecs(60 * i), -3000);
|
||||
|
||||
const QVariantMap r = rpc("GetOptimizerProtocol");
|
||||
QCOMPARE(r.value("protocolVersion").toString(), QString("1.1"));
|
||||
const QJsonObject servi = QJsonObject::fromVariantMap(r.value("schema").toMap());
|
||||
const QJsonObject embarque = QJsonDocument::fromJson(OptimizerProtocol::schemaDocumentBytes()).object();
|
||||
QVERIFY(!embarque.isEmpty());
|
||||
QCOMPARE(servi.value("openrpc").toString(), QString("1.3.2"));
|
||||
QVERIFY2(servi == embarque, "GetOptimizerProtocol ne sert pas le contrat embarqué");
|
||||
QFile f(Contrat::schemaDuDepot());
|
||||
QVERIFY(f.open(QIODevice::ReadOnly));
|
||||
QVERIFY2(QJsonDocument::fromJson(f.readAll()).object() == servi, "le contrat servi n'est pas celui de docs/");
|
||||
|
||||
// Jugé avec le contrat SERVI, pas une copie : c'est celui qu'un auteur d'optimiseur lirait.
|
||||
QTemporaryDir dir;
|
||||
QVERIFY(dir.isValid());
|
||||
const QList<QJsonObject> requetes = FakeOptimizer::capture();
|
||||
FakeOptimizer::capture().clear();
|
||||
const Contrat::Verdict v = Contrat::juger(requetes, Contrat::ecrireSchema(dir, QJsonDocument(servi).toJson()),
|
||||
{QStringLiteral("--requests-only")});
|
||||
QVERIFY2(v.code == 0, qUtf8Printable(v.sortie.right(4000)));
|
||||
QVERIFY2(v.compte("Handshake.params") >= 1, qUtf8Printable(v.sortie.right(1000)));
|
||||
QVERIFY2(v.compte("GetPlan.params") >= 3, qUtf8Printable(v.sortie.right(1000)));
|
||||
}
|
||||
|
||||
#include "testsocketscheduler.moc"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user