- OPTIMIZER_DECISION : code générique d'une action d'optimiseur dont le reasonCode est absent, inconnu ou hors liste blanche ; sans paramètres ni texte (invariant 12). - PRICE_SLOT (priceCtkWh, o:setpointW) : décision prise sur le prix du créneau, demandée par l'app (exigence 5). Aucun émetteur interne — le rule-based ne lit pas le prix (§11). EV_SPOT_MARKET ne convenait pas : il nomme un fournisseur hors du build et ne vaut que pour une borne. - DecisionCode::knownCodes() : la liste énumérable du catalogue (« connu » se lit ici). - DecisionCode::optimizerEmittableCodes() / optimizerMayEmit() : liste blanche E14 — SURPLUS_SETPOINT, SURPLUS_INSUFFICIENT, BELOW_MIN_POWER, EV_DEADLINE, SG_FORCED, SG_RECOMMENDED, SG_NORMAL, ECO_FLOOR_GRID, PRICE_SLOT, OPTIMIZER_DECISION. Jamais la sécurité, les verrous, l'indisponibilité, le plafond, la réserve, la dérogation ni les états du moteur. - renderFr : deux branches ; INTERFACE.md : les deux lignes (libellés FR/DE/EN proposés pour l'app) et l'encadré de la liste blanche. Rien n'appelle ces fonctions : sans endpoint, aucun effet. Tests (loadmodel) : anti-dérive — knownCodes() confronté aux constantes de l'EN-TÊTE lu par le test, chacune avec un rendu autre que « motif inconnu » ; liste blanche ⊂ catalogue et sans aucun code moteur. Contre-épreuves : constante ajoutée sans entrée, branche SG_NORMAL retirée → échec. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83 lines
4.7 KiB
C++
83 lines
4.7 KiB
C++
// 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 <QFile>
|
|
#include <QRegularExpression>
|
|
#include <QSet>
|
|
#include <QTest>
|
|
|
|
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<QString> 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<QString>(connus.cbegin(), connus.cend()).size(), connus.size()); // sans doublon
|
|
QCOMPARE(QSet<QString>(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"));
|
|
}
|