test(socketscheduler) : ce qu'une configuration héritée hors règle envoie dans GetPlan
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 <noreply@anthropic.com>
This commit is contained in:
parent
a728a3c8da
commit
8988af0a16
@ -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 <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QLoggingCategory>
|
||||
#include <QRegularExpression>
|
||||
#include <QSignalSpy>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
@ -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<QVariantMap> 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<QString, QVariantMap> 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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user