LoadConfigStore : un libellé hérité n'est cité que sur 64 caractères dans les refus d'ensemble (m-1)

appendConfig() ne borne que l'entrée ajoutée, mais validateSet() (THING_SHARED) et
validateRanks() (RANK_DUPLICATE) citaient en entier les libellés des deux entrées en
conflit — dont une héritée, sans borne. provisionEvChargerConfig() journalise cette phrase.

Les libellés y passent par libellePourMotif() : 64 caractères au plus (leftChars, jamais au
milieu d'une paire), suivis de « … ». Le motif publié (faultParams) ne change pas.

Test : testInheritedLabelIsCutInSetRefusals — héritée de 300 caractères ; l'ajout d'une borne
refusé sur THING_SHARED puis sur RANK_DUPLICATE : motif exact inchangé, phrase avec les 64
premiers caractères et sans le libellé entier.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-10-22 23:22:58 +02:00
parent 58380dd7db
commit 76b348bf8b
4 changed files with 55 additions and 4 deletions

View File

@ -33,6 +33,14 @@ QString valeurPourMotif(const QString &valeur)
: LoadConfigStore::leftChars(valeur, 16) + QStringLiteral("…"); : LoadConfigStore::leftChars(valeur, 16) + QStringLiteral("…");
} }
//! Un libellé cité dans un motif : ses 64 premiers caractères. Celui d'une entrée héritée n'est
//! pas borné, et l'ajout automatique d'une borne journalise ce motif (relecture q7-suite, m-1).
QString libellePourMotif(const QString &libelle)
{
return libelle.toUcs4().size() <= 64 ? libelle
: LoadConfigStore::leftChars(libelle, 64) + QStringLiteral("…");
}
//! Une valeur citée au journal, entre guillemets et ÉCHAPPÉE par Qt (\c "\n" pour un saut de //! Une valeur citée au journal, entre guillemets et ÉCHAPPÉE par Qt (\c "\n" pour un saut de
//! ligne) : une valeur persistée par un client ne fabrique pas de fausse ligne de journal. //! ligne) : une valeur persistée par un client ne fabrique pas de fausse ligne de journal.
QString citee(const QString &valeur) QString citee(const QString &valeur)
@ -203,7 +211,7 @@ bool LoadConfigStore::validateSet(const LoadConfigs &configs, QString *error, QV
return fail(QStringLiteral("le Thing %1 est commandé par deux charges actives, " return fail(QStringLiteral("le Thing %1 est commandé par deux charges actives, "
"\"%2\" et \"%3\" : deux commandes contradictoires et " "\"%2\" et \"%3\" : deux commandes contradictoires et "
"une puissance comptée deux fois") "une puissance comptée deux fois")
.arg(thing, configs.at(j).label(), c.label()), .arg(thing, libellePourMotif(configs.at(j).label()), libellePourMotif(c.label())),
QVariantMap{{QStringLiteral("cause"), QStringLiteral("THING_SHARED")}, QVariantMap{{QStringLiteral("cause"), QStringLiteral("THING_SHARED")},
{QStringLiteral("indexes"), QVariantList{j, i}}, {QStringLiteral("indexes"), QVariantList{j, i}},
{QStringLiteral("ids"), QVariantList{configs.at(j).id(), c.id()}}}); {QStringLiteral("ids"), QVariantList{configs.at(j).id(), c.id()}}});
@ -225,7 +233,8 @@ bool LoadConfigStore::validateRanks(const LoadConfigs &configs, QString *error,
*error = QStringLiteral("les charges \"%1\" et \"%2\" portent toutes deux le rang " *error = QStringLiteral("les charges \"%1\" et \"%2\" portent toutes deux le rang "
"%3 : leur ordre de service serait stable mais NON " "%3 : leur ordre de service serait stable mais NON "
"RÉGLABLE — il se déciderait sur leurs identifiants") "RÉGLABLE — il se déciderait sur leurs identifiants")
.arg(configs.at(j).label(), c.label()).arg(c.priority()); .arg(libellePourMotif(configs.at(j).label()), libellePourMotif(c.label()))
.arg(c.priority());
if (fault) if (fault)
*fault = QVariantMap{{QStringLiteral("cause"), QStringLiteral("RANK_DUPLICATE")}, *fault = QVariantMap{{QStringLiteral("cause"), QStringLiteral("RANK_DUPLICATE")},
{QStringLiteral("indexes"), QVariantList{j, i}}, {QStringLiteral("indexes"), QVariantList{j, i}},

View File

@ -83,7 +83,8 @@ public:
* \c setConfigs(), donc par cette vérification. * \c setConfigs(), donc par cette vérification.
* *
* \param configs Ensemble complet à confronter (les charges désactivées sont ignorées). * \param configs Ensemble complet à confronter (les charges désactivées sont ignorées).
* \param[out] error Motif de refus, en français, exploitable tel quel. * \param[out] error Motif de refus, en français, exploitable tel quel ; un libellé n'y est
* cité que sur ses 64 premiers caractères (celui d'une entrée héritée n'est pas borné).
* \param[out] fault Si refus : \c ID_DUPLICATE (\c index, \c id) ou \c THING_SHARED * \param[out] fault Si refus : \c ID_DUPLICATE (\c index, \c id) ou \c THING_SHARED
* (\c indexes, \c ids — la paire de charges). * (\c indexes, \c ids — la paire de charges).
* \return true si l'ensemble est cohérent ; false au PREMIER conflit rencontré. * \return true si l'ensemble est cohérent ; false au PREMIER conflit rencontré.
@ -94,7 +95,8 @@ public:
/*! /*!
* \brief Contrôle de BONNE FORME, appliqué aux ÉCRITURES et à elles seules. §12 / LM-1209-a. * \brief Contrôle de BONNE FORME, appliqué aux ÉCRITURES et à elles seules. §12 / LM-1209-a.
* *
* \param configs Ensemble candidat. \param error Motif du refus, si refus. * \param configs Ensemble candidat. \param error Motif du refus, si refus ; un libellé n'y
* est cité que sur ses 64 premiers caractères.
* \param[out] fault Si refus : \c RANK_DUPLICATE (\c indexes, \c ids, \c rank). * \param[out] fault Si refus : \c RANK_DUPLICATE (\c indexes, \c ids, \c rank).
* \return Faux si deux charges portent le même rang. * \return Faux si deux charges portent le même rang.
* *

View File

@ -957,6 +957,44 @@ void TestLoadModel::testInheritedValuesAreEscapedInTheJournal()
QVERIFY2(journal.faussesLignes().isEmpty(), qUtf8Printable(journal.faussesLignes().join(QStringLiteral(" | ")))); QVERIFY2(journal.faussesLignes().isEmpty(), qUtf8Printable(journal.faussesLignes().join(QStringLiteral(" | "))));
} }
void TestLoadModel::testInheritedLabelIsCutInSetRefusals()
{
// [m-1] L'ajout d'une borne ne borne que l'entrée ajoutée ; ses refus d'ENSEMBLE citaient
// pourtant le libellé hérité en entier, et provisionEvChargerConfig() le journalise.
const QString cfgPath = QDir::tempPath() + "/etm-loadcfg-libelle-motif.json";
QFile::remove(cfgPath);
qputenv("NYMEA_ENERGY_LOAD_CONFIG", cfgPath.toUtf8());
const auto nettoyer = qScopeGuard([&] { QFile::remove(cfgPath); });
// Héritée : rang 2, libellé de 300 caractères, et elle commande le Thing de la borne.
const QString tropLong = QString(299, QLatin1Char('L')) + QLatin1Char('Z');
QVariantMap herite = ballonMap();
herite.insert("label", tropLong);
herite.insert("priority", 2);
herite.insert("relays", QVariantList{QVariantMap{{"thingId", BORNE}, {"powerW", 1000}}});
ecrireFichier(cfgPath, {herite});
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(QStringLiteral("LIBELLÉ TROP LONG")));
LoadConfigStore store;
QCOMPARE(store.configs().count(), 1);
const QString cite = LoadConfigStore::leftChars(tropLong, 64) + QStringLiteral("…");
QString err;
QVariantMap motif;
// THING_SHARED : le motif publié est inchangé, la phrase ne cite que 64 caractères.
QVERIFY(!store.appendConfig(LoadConfig::fromMap(borneMap(BORNE, 3)), &err, &motif));
QCOMPARE(json(motif), json(QVariantMap{{"cause", "THING_SHARED"}, {"indexes", QVariantList{0, 1}},
{"ids", QVariantList{"ballon-a", BORNE}}}));
QVERIFY2(err.contains(cite) && !err.contains(tropLong), qUtf8Printable(err));
// RANK_DUPLICATE : idem.
const QString autreBorne = QStringLiteral("{dddddddd-0000-0000-0000-0000000000f2}");
QVERIFY(!store.appendConfig(LoadConfig::fromMap(borneMap(autreBorne, 2)), &err, &motif));
QCOMPARE(json(motif), json(QVariantMap{{"cause", "RANK_DUPLICATE"}, {"indexes", QVariantList{0, 1}},
{"ids", QVariantList{"ballon-a", autreBorne}}, {"rank", 2}}));
QVERIFY2(err.contains(cite) && !err.contains(tropLong), qUtf8Printable(err));
QCOMPARE(store.configs().count(), 1);
}
void TestLoadModel::testRankOriginFollowsTheRankNotTheWrite() void TestLoadModel::testRankOriginFollowsTheRankNotTheWrite()
{ {
// [LM-1209-c] La marque dit « ce rang est un défaut, pas un choix ». Ce qui la lève est // [LM-1209-c] La marque dit « ce rang est un défaut, pas un choix ». Ce qui la lève est

View File

@ -96,6 +96,8 @@ private slots:
void testAppendConfigBoundsOnlyTheAddedEntry(); void testAppendConfigBoundsOnlyTheAddedEntry();
//! Relecture q7-suite : une valeur héritée citée par load() est échappée — aucune fausse ligne. //! Relecture q7-suite : une valeur héritée citée par load() est échappée — aucune fausse ligne.
void testInheritedValuesAreEscapedInTheJournal(); void testInheritedValuesAreEscapedInTheJournal();
//! Relecture q7-suite (m-1) : un refus d'ensemble ne cite que 64 caractères d'un libellé hérité.
void testInheritedLabelIsCutInSetRefusals();
// ── LM-1006-1 — le plafond de soutirage, étape 1 ─────────────────────── // ── LM-1006-1 — le plafond de soutirage, étape 1 ───────────────────────
//! On compare des MARGES, jamais des plafonds : le plus contraignant mord, et on sait //! On compare des MARGES, jamais des plafonds : le plus contraignant mord, et on sait