From 8988af0a165e5090353ffc7f3860a1620449ab23 Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Thu, 22 Oct 2026 20:26:17 +0200 Subject: [PATCH] =?UTF-8?q?test(socketscheduler)=20:=20ce=20qu'une=20confi?= =?UTF-8?q?guration=20h=C3=A9rit=C3=A9e=20hors=20r=C3=A8gle=20envoie=20dan?= =?UTF-8?q?s=20GetPlan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit testInheritedOutOfRuleConfigWhatGetPlanSends : une configuration écrite sur disque avant la règle (identifiant « ballon test », échéances « 07:00 » et « 7:00 ») est chargée par un store neuf et signalée. Lu sur le faux optimiseur : l'identifiant hors règle part tel quel ; l'échéance lisible part avec ses energyTargets (contre-épreuve) ; l'échéance illisible ne part ni brute ni résolue, minEnergyWhPerDay part encore. La requête est jugée conforme au contrat embarqué (LoadNeeds.dailyDeadline impose ^[0-9]{2}:[0-9]{2}$, que « 7:00 » violait). Co-Authored-By: Claude Opus 5.5 --- .../socketscheduler/testsocketscheduler.cpp | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/auto/socketscheduler/testsocketscheduler.cpp b/tests/auto/socketscheduler/testsocketscheduler.cpp index 3a84fe3..51dc161 100644 --- a/tests/auto/socketscheduler/testsocketscheduler.cpp +++ b/tests/auto/socketscheduler/testsocketscheduler.cpp @@ -13,6 +13,7 @@ #include "energysettings.h" #include "etm/energyarbitrator.h" +#include "etm/config/loadconfigstore.h" #include "etm/scheduler/socketscheduler.h" #include "etm/scheduler/optimizerstatus.h" #include "etm/scheduler/publishedplan.h" @@ -24,6 +25,7 @@ #include #include #include +#include #include #include @@ -127,6 +129,8 @@ private slots: void testUpdateLearnedIsLoggedOnceAndIgnored(); // Protocole 1.2, Q7 — une requête trop longue n'est pas envoyée, et l'échec compte void testARequestTooLongCountsAsAFailure(); + // Q7, suite — une configuration héritée hors règle, et ce qui part réellement dans GetPlan + void testInheritedOutOfRuleConfigWhatGetPlanSends(); // Brief 4 T6 — traces de décision (annexe D) void testTraceIsOffByDefault(); void testTraceSettingByRpc(); @@ -182,6 +186,9 @@ private: void amenerAOptimiseur(const QDateTime &t0, int validiteS = 1800); //! Triplets publiés par les notifications \c OptimizerStatusChanged de l'arbitre. static QStringList triplets(const QSignalSpy &spy); + //! Q7, suite : remplace la configuration par \p charges ÉCRITES SUR DISQUE et relues par un + //! store neuf — le chemin d'une configuration héritée, que SetLoadConfig refuserait. + void chargerConfigHeritee(const QVariantList &charges); //! T6 : active les traces dans un répertoire temporaire propre ; rend son chemin. QString activerTraces(); //! T6 : les lignes de trace de \p dossier, tous fichiers, dans l'ordre ; attend d'en avoir @@ -1565,6 +1572,63 @@ QList TestSocketScheduler::traces(const QString &dossier, int n) return lignes; } +void TestSocketScheduler::chargerConfigHeritee(const QVariantList &charges) +{ + QFile f(qEnvironmentVariable("NYMEA_ENERGY_LOAD_CONFIG")); + QVERIFY(f.open(QIODevice::WriteOnly | QIODevice::Text)); + f.write(QJsonDocument(QJsonObject{{"version", 1}, {"loads", QJsonArray::fromVariantList(charges)}}).toJson()); + f.close(); + m_arb->setLoadConfigStore(new LoadConfigStore(m_arb)); +} + +void TestSocketScheduler::testInheritedOutOfRuleConfigWhatGetPlanSends() +{ + // Q7, suite : une configuration ENREGISTRÉE avant la règle est chargée, signalée — et voici ce + // qui en part réellement vers l'optimiseur, lu sur le faux optimiseur. Un identifiant hors + // règle part tel quel (c'est une clé : le changer serait retirer une charge) ; une échéance + // illisible NE PART PAS — le contrat (LoadNeeds.dailyDeadline, motif ^[0-9]{2}:[0-9]{2}$) + // l'aurait de toute façon déclarée non conforme. + monter(); + m_repondre = [](const QVariantMap &) { return QVariantMap{{"abstain", true}, {"reason", "test"}}; }; + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + QStringLiteral("IDENTIFIANT HORS RÈGLE : charge \"ballon test\""))); + QTest::ignoreMessage(QtWarningMsg, QRegularExpression( + QStringLiteral("ÉCHÉANCE ILLISIBLE : charge \"%1\", needs.dailyDeadline \"7:00\"") + .arg(QRegularExpression::escape(m_var)))); + chargerConfigHeritee({ + QVariantMap{{"id", "ballon test"}, {"label", "Ballon"}, {"adapter", "relay-router"}, {"mode", "fixed"}, + {"priority", 1}, {"enabled", true}, {"minOnS", 0}, {"minOffS", 0}, + {"relays", QVariantList{QVariantMap{{"thingId", m_relais.toString()}, {"powerW", 1000}}}}, + {"needs", QVariantMap{{"dailyDeadline", "07:00"}, {"minEnergyWhPerDay", 1000}}}}, + QVariantMap{{"id", m_var}, {"label", "Routeur"}, {"adapter", "etmvariableload"}, {"mode", "dynamic"}, + {"maxPowerW", 3000}, {"priority", 2}, {"enabled", true}, + {"needs", QVariantMap{{"dailyDeadline", "7:00"}, {"minEnergyWhPerDay", 2000}}}}}); + + cycle(utcDateTime(QDate(2026, 6, 8), QTime(13, 0, 0)), -3000); + QVERIFY(!m_requetes.isEmpty()); + QHash charges; + for (const QVariant &v : m_requetes.last().value("loads").toList()) + charges.insert(v.toMap().value("id").toString(), v.toMap()); + + // L'identifiant hors règle part TEL QUEL, et l'échéance lisible avec lui (contre-épreuve) : + // « 07:00 » est envoyée, et l'obligation a son échéance absolue. + QVERIFY2(charges.contains("ballon test"), qUtf8Printable(charges.keys().join(", "))); + const QVariantMap besoinsLisibles = charges.value("ballon test").value("needs").toMap(); + QCOMPARE(besoinsLisibles.value("dailyDeadline").toString(), QStringLiteral("07:00")); + QVERIFY(!besoinsLisibles.value("energyTargets").toList().isEmpty()); + + // L'échéance illisible est ABSENTE : ni brute, ni résolue. L'obligation en énergie part + // encore — sans échéance, elle n'a pas de cadence, et rien n'est acheté pour elle. + QVERIFY(charges.contains(m_var)); + const QVariantMap besoins = charges.value(m_var).value("needs").toMap(); + QVERIFY2(!besoins.contains("dailyDeadline"), qUtf8Printable(QString::fromUtf8(QJsonDocument::fromVariant(besoins).toJson()))); + QVERIFY(!besoins.contains("energyTargets")); + QCOMPARE(besoins.value("minEnergyWhPerDay").toInt(), 2000); + + // Et ce qui est parti est CONFORME au contrat publié. + jugerLesRequetesDuMoteur(); +} + void TestSocketScheduler::testARequestTooLongCountsAsAFailure() { // Protocole 1.2, Q7 : une requête que le client n'émet pas (ligne de plus de 1 Mio) est un