// SPDX-License-Identifier: GPL-3.0-or-later // // Brief 4 T5 — catalogue des motifs : liste énumérable, anti-dérive, liste blanche optimiseur. // Logique pure, comme le reste de cette suite. #include "testloadmodel.h" #include "etm/types/decisionreason.h" #include #include #include #include void TestLoadModel::testDecisionCodesListIsComplete() { // ANTI-DÉRIVE : la liste énumérable est confrontée à l'EN-TÊTE lui-même, pas à une copie. // Une constante ajoutée sans ligne dans knownCodes() fait échouer ce test. QFile f(QStringLiteral(ETM_DECISIONREASON_H)); QVERIFY2(f.open(QIODevice::ReadOnly), ETM_DECISIONREASON_H); const QString source = QString::fromUtf8(f.readAll()); const int debut = source.indexOf(QStringLiteral("namespace DecisionCode {")); const int fin = source.indexOf(QStringLiteral("\n}"), debut); QVERIFY(debut >= 0 && fin > debut); const QRegularExpression constante(QStringLiteral(R"re(constexpr\s+const\s+char\s*\*\s*\w+\s*=\s*"(\w+)")re")); QSet declares; auto it = constante.globalMatch(source.mid(debut, fin - debut)); while (it.hasNext()) declares.insert(it.next().captured(1)); QVERIFY(declares.size() > 30); // garde de vraisemblance : l'expression a bien mordu const QStringList connus = DecisionCode::knownCodes(); QCOMPARE(QSet(connus.cbegin(), connus.cend()).size(), connus.size()); // sans doublon QCOMPARE(QSet(connus.cbegin(), connus.cend()), declares); // Et chacun a une branche de rendu : jamais le repli « motif inconnu ». for (const QString &code : connus) { const QString rendu = renderFr(DecisionReason(code.toLatin1().constData()), QStringLiteral("Charge")); QVERIFY2(!rendu.startsWith(QStringLiteral("(motif inconnu")), qUtf8Printable(code)); } } void TestLoadModel::testOptimizerMayEmitOnlyWhitelistedCodes() { const QStringList liste = DecisionCode::optimizerEmittableCodes(); const QStringList connus = DecisionCode::knownCodes(); for (const QString &c : liste) { QVERIFY2(connus.contains(c), qUtf8Printable(c)); QVERIFY(DecisionCode::optimizerMayEmit(c)); } // E14 — un optimiseur ne peut pas parler au nom de la sécurité, du cycle de vie, d'un // verrou, ni d'un état du moteur : ces motifs décrivent ce que le MOTEUR a fait. for (const char *c : {DecisionCode::DegradedL2, DecisionCode::SafeStateRelay, DecisionCode::SafeStateSetpoint, DecisionCode::SafeStateSgReady, DecisionCode::LoadUnavailable, DecisionCode::DrawCap, DecisionCode::PhaseLimit, DecisionCode::DerogationActive, DecisionCode::DerogationEnded, DecisionCode::EcoFloorMet, DecisionCode::EcoFloorMissed, DecisionCode::LockMinOn, DecisionCode::LockMinOff, DecisionCode::LockMinOffCapped, DecisionCode::LockMinStateHold, DecisionCode::RecreditSuspended, DecisionCode::BatteryReserve, DecisionCode::EvGridStart, DecisionCode::EvSurplus, DecisionCode::EvEcoMin, DecisionCode::EvIdle, DecisionCode::MissingForecastSite, DecisionCode::ForecastPayloadRefused, DecisionCode::ForecastDateUnreadable, DecisionCode::TariffRefused, DecisionCode::SpotMarketNotInBuild}) { QVERIFY2(!DecisionCode::optimizerMayEmit(QString::fromLatin1(c)), c); } QVERIFY(!DecisionCode::optimizerMayEmit(QStringLiteral("CODE_QUI_NEXISTE_PAS"))); QVERIFY(!DecisionCode::optimizerMayEmit(QString())); // Le code générique et le motif prix (exigence app 5) en font partie. QVERIFY(DecisionCode::optimizerMayEmit(QString::fromLatin1(DecisionCode::OptimizerDecision))); QVERIFY(DecisionCode::optimizerMayEmit(QString::fromLatin1(DecisionCode::PriceSlot))); // Rendus : le générique ne porte aucun texte de l'optimiseur ; le prix se lit avec sa valeur. QCOMPARE(renderFr(DecisionReason(DecisionCode::OptimizerDecision), QStringLiteral("Chauffe-eau")), QStringLiteral("Décision de l'optimiseur — Chauffe-eau (motif hors catalogue)")); QCOMPARE(renderFr(DecisionReason(DecisionCode::PriceSlot, {{"priceCtkWh", 16.96}, {"setpointW", 2400}}), QStringLiteral("Chauffe-eau")), QStringLiteral("Prix favorable (16,96 ct/kWh) — Chauffe-eau consigne 2400 W")); QCOMPARE(renderFr(DecisionReason(DecisionCode::PriceSlot, {{"priceCtkWh", 9.5}}), QStringLiteral("PAC")), QStringLiteral("Prix favorable (9,5 ct/kWh) — PAC servie sur ce créneau")); }