SetLoadConfig : libellé de charge borné à 256 caractères, à l'écriture seulement (Q7)
Le libellé part tel quel dans chaque GetPlan. Rien ne le bornait : une saisie de plus de 1 Mio suffisait à faire dépasser la limite de ligne du protocole. La borne arrête la saisie en amont ; le refus d'émission du client (commit précédent) reste le filet. - LoadConfigStore::validateLabels(), appelée en tête de setConfigs() : un libellé de plus de 256 points de code Unicode → refus en bloc, rien n'est modifié ; SetLoadConfig rend EnergyErrorInvalidParameter et journalise le motif en français, qui nomme la charge par son id et donne la longueur, jamais le libellé. Placée AVANT isValid() parce que les motifs suivants citent le libellé. - Même partage que les rangs (LM-1209-a-bis) : load() ne l'applique pas. Un libellé plus long déjà persisté est chargé tel quel, ni tronqué ni écarté, et signalé ; la première écriture devra le raccourcir. - Test : testLabelIsBoundedOnWriteNeverOnLoad (256 accepté, 257 refusé sans modification en mémoire ni sur disque, configuration héritée conservée entière). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
cd91baf98f
commit
6b1dd95e2e
@ -89,6 +89,15 @@ bool LoadConfigStore::load()
|
||||
<< "mais il n'est PAS réglable tant que les rangs sont à égalité : il obéit à"
|
||||
<< "des identifiants, donc à personne. À réparer par un SetLoadConfig.";
|
||||
}
|
||||
// Q7 — même règle pour un libellé trop long : CHARGÉ TEL QUEL, jamais tronqué ni écarté. Le
|
||||
// client d'optimiseur refuse d'émettre un GetPlan de plus de 1 Mio ; c'est lui le filet.
|
||||
{
|
||||
QString pourquoi;
|
||||
if (!validateLabels(loaded, &pourquoi))
|
||||
qCWarning(dcNymeaEnergy()).noquote()
|
||||
<< "[LoadConfigStore] CONFIGURATION CONSERVÉE, LIBELLÉ TROP LONG :" << pourquoi
|
||||
<< "— chargé tel quel ; toute écriture sera refusée tant qu'il n'est pas raccourci.";
|
||||
}
|
||||
|
||||
m_configs = loaded;
|
||||
return true;
|
||||
@ -196,8 +205,27 @@ bool LoadConfigStore::reconcileRankOrigin(const LoadConfigs &incoming, LoadConfi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LoadConfigStore::validateLabels(const LoadConfigs &configs, QString *error)
|
||||
{
|
||||
for (const LoadConfig &c : configs) {
|
||||
const qsizetype caracteres = c.label().toUcs4().size();
|
||||
if (caracteres > MaxLabelChars) {
|
||||
if (error)
|
||||
*error = QStringLiteral("le libellé de la charge \"%1\" compte %2 caractères, %3 au plus")
|
||||
.arg(c.id().left(64)).arg(caracteres).arg(MaxLabelChars);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LoadConfigStore::setConfigs(const LoadConfigs &configs, QString *error)
|
||||
{
|
||||
// Les libellés D'ABORD : les motifs de refus suivants citent le libellé, et un libellé
|
||||
// démesuré ne doit entrer ni dans la configuration ni dans le journal (Q7).
|
||||
if (!validateLabels(configs, error))
|
||||
return false;
|
||||
|
||||
// Validation EN BLOC : une seule entrée invalide → rejet total (rien persisté).
|
||||
for (const LoadConfig &c : configs) {
|
||||
QString why;
|
||||
|
||||
@ -86,6 +86,27 @@ public:
|
||||
*/
|
||||
static bool validateRanks(const LoadConfigs &configs, QString *error = nullptr);
|
||||
|
||||
//! Plus long libellé de charge accepté à l'écriture, en caractères (points de code Unicode).
|
||||
static constexpr int MaxLabelChars = 256;
|
||||
|
||||
/*!
|
||||
* \brief Borne la longueur des libellés, à l'ÉCRITURE seulement (protocole optimiseur 1.2, Q7).
|
||||
*
|
||||
* Le libellé part tel quel dans chaque \c GetPlan (§5.3). Sans borne, une saisie démesurée
|
||||
* ferait dépasser la limite de 1 Mio par ligne du protocole, et l'optimiseur ne recevrait plus
|
||||
* rien. Le client d'optimiseur refuse d'émettre une telle ligne (\c request-too-long) : cette
|
||||
* borne-ci arrête la saisie avant qu'elle n'y arrive.
|
||||
*
|
||||
* \param configs Ensemble candidat. \param[out] error Motif du refus, en français ; il nomme
|
||||
* la charge par son identifiant et donne la longueur, jamais le libellé.
|
||||
* \return Faux si un libellé compte plus de \c MaxLabelChars caractères.
|
||||
*
|
||||
* \note Même famille que \c validateRanks() (\rule{LM-1209-a-bis}) : un refus d'écriture, jamais
|
||||
* un écart au chargement. \c load() ne l'appelle pas — un libellé plus long déjà persisté est
|
||||
* **chargé tel quel, signalé, jamais tronqué** ; la première écriture devra le raccourcir.
|
||||
*/
|
||||
static bool validateLabels(const LoadConfigs &configs, QString *error = nullptr);
|
||||
|
||||
/*!
|
||||
* \brief Réconcilie \c rankOrigin d'une écriture CLIENTE avec l'état persisté. LM-1209-c.
|
||||
*
|
||||
|
||||
@ -471,6 +471,66 @@ void TestLoadModel::testRankUniquenessRefusesWriteButNeverDiscards()
|
||||
QCOMPARE(relu.configs().count(), 2);
|
||||
}
|
||||
|
||||
void TestLoadModel::testLabelIsBoundedOnWriteNeverOnLoad()
|
||||
{
|
||||
// Protocole optimiseur 1.2, Q7 : le libellé part tel quel dans chaque GetPlan. Borné à
|
||||
// l'ÉCRITURE (256 caractères), jamais au chargement — même partage que les rangs (LM-1209-a) :
|
||||
// une règle resserrée après coup refuse une écriture, elle ne fait pas disparaître une charge.
|
||||
const QString cfgPath = QDir::tempPath() + "/etm-loadcfg-labels.json";
|
||||
QFile::remove(cfgPath);
|
||||
qputenv("NYMEA_ENERGY_LOAD_CONFIG", cfgPath.toUtf8());
|
||||
QString err;
|
||||
|
||||
// La borne compte des CARACTÈRES : 256 « é » (deux octets UTF-8 chacun) passent, et un
|
||||
// caractère hors plan de base (deux unités UTF-16) compte pour un.
|
||||
auto avecLibelle = [](const QString &label) {
|
||||
QVariantMap m = borneMap(BORNE, 1);
|
||||
m.insert("label", label);
|
||||
return LoadConfigs(QList<LoadConfig>{LoadConfig::fromMap(m)});
|
||||
};
|
||||
const QString limite = QString(LoadConfigStore::MaxLabelChars - 1, QChar(0x00e9)) + QString::fromUcs4(U"\U0001F50C");
|
||||
QCOMPARE(limite.toUcs4().size(), qsizetype(256));
|
||||
QVERIFY(limite.size() > 256); // en unités UTF-16, il dépasserait : c'est bien le caractère qui compte
|
||||
|
||||
LoadConfigStore store;
|
||||
QVERIFY2(store.setConfigs(avecLibelle(limite), &err), qUtf8Printable(err));
|
||||
QCOMPARE(store.configs().at(0).label(), limite);
|
||||
QFile f(cfgPath);
|
||||
QVERIFY(f.open(QIODevice::ReadOnly));
|
||||
const QByteArray persiste = f.readAll();
|
||||
f.close();
|
||||
|
||||
// 257 : refusé, RIEN n'est modifié — ni en mémoire, ni sur disque. Le motif nomme la charge
|
||||
// par son identifiant et donne la longueur, jamais le libellé.
|
||||
const QString tropLong = QString(LoadConfigStore::MaxLabelChars + 1, QLatin1Char('L'));
|
||||
QVERIFY(!store.setConfigs(avecLibelle(tropLong), &err));
|
||||
QVERIFY2(err.contains(QStringLiteral("257 caractères")) && err.contains(BORNE), qUtf8Printable(err));
|
||||
QVERIFY(!err.contains(tropLong.left(32)));
|
||||
QCOMPARE(store.configs().count(), 1);
|
||||
QCOMPARE(store.configs().at(0).label(), limite);
|
||||
QVERIFY(f.open(QIODevice::ReadOnly));
|
||||
QCOMPARE(f.readAll(), persiste);
|
||||
f.close();
|
||||
|
||||
// ---- Au CHARGEMENT : une configuration héritée garde son libellé, entier ----------------
|
||||
QVariantMap herite = borneMap(BORNE, 1);
|
||||
herite.insert("label", tropLong);
|
||||
QVERIFY(f.open(QIODevice::WriteOnly | QIODevice::Text));
|
||||
f.write(QJsonDocument(QJsonObject{{"version", 1}, {"loads", QJsonArray{QJsonObject::fromVariantMap(herite)}}}).toJson());
|
||||
f.close();
|
||||
LoadConfigStore relu;
|
||||
QCOMPARE(relu.configs().count(), 1);
|
||||
QCOMPARE(relu.configs().at(0).label(), tropLong); // ni écarté, ni tronqué
|
||||
|
||||
// L'aller-retour verbatim est refusé tant que le libellé n'est pas raccourci ; raccourci, il passe.
|
||||
QVERIFY(!relu.setConfigs(relu.configs(), &err));
|
||||
QCOMPARE(relu.configs().at(0).label(), tropLong);
|
||||
LoadConfigs repare = relu.configs();
|
||||
repare[0].setLabel(QStringLiteral("Borne garage"));
|
||||
QVERIFY2(relu.setConfigs(repare, &err), qUtf8Printable(err));
|
||||
QFile::remove(cfgPath);
|
||||
}
|
||||
|
||||
void TestLoadModel::testRankOriginFollowsTheRankNotTheWrite()
|
||||
{
|
||||
// [LM-1209-c] La marque dit « ce rang est un défaut, pas un choix ». Ce qui la lève est
|
||||
|
||||
@ -75,6 +75,8 @@ private slots:
|
||||
//! La marque de rang s'efface sur un RANG changé, pas sur une écriture reçue — et un
|
||||
//! client ne peut pas fabriquer un "auto" (LM-1209-c).
|
||||
void testRankOriginFollowsTheRankNotTheWrite();
|
||||
// Protocole optimiseur 1.2, Q7 — le libellé est borné à l'écriture, jamais au chargement
|
||||
void testLabelIsBoundedOnWriteNeverOnLoad();
|
||||
|
||||
// ── LM-1006-1 — le plafond de soutirage, étape 1 ───────────────────────
|
||||
//! On compare des MARGES, jamais des plafonds : le plus contraignant mord, et on sait
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user