LoadConfigStore::load() : les valeurs héritées sont citées échappées au journal
Même défaut que I-1, dans les avertissements de chargement ajoutés par Q7 : l'identifiant hors règle et l'échéance illisible étaient cités en noquote(). Avant Q7, un client pouvait les persister avec un saut de ligne (ni l'id ni l'échéance n'étaient bornés), et la lecture du fichier fabriquait alors une fausse ligne de journal. Ces valeurs passent par citee() : entre guillemets, échappées par QDebug (même rendu qu'avant pour une valeur ordinaire). L'avertissement LIBELLÉ TROP LONG, qui cite l'identifiant dans sa phrase, perd son noquote(). Test : testInheritedValuesAreEscapedInTheJournal charge un fichier dont l'id et l'échéance valent "x\n[NymeaEnergy] FAUX" : les trois avertissements partent, citent la valeur échappée, et aucun message n'ajoute de ligne commençant par [NymeaEnergy]. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2ce74892d3
commit
58380dd7db
@ -6,6 +6,7 @@
|
||||
|
||||
#include <nymeasettings.h>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QSaveFile>
|
||||
@ -32,6 +33,15 @@ QString valeurPourMotif(const QString &valeur)
|
||||
: LoadConfigStore::leftChars(valeur, 16) + QStringLiteral("…");
|
||||
}
|
||||
|
||||
//! 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.
|
||||
QString citee(const QString &valeur)
|
||||
{
|
||||
QString s;
|
||||
QDebug(&s).nospace() << valeur;
|
||||
return s;
|
||||
}
|
||||
|
||||
//! Le motif publiable d'un refus PAR ENTRÉE : la cause, la position, et l'identifiant quand il
|
||||
//! est bien formé — un identifiant refusé ne se recopie pas.
|
||||
QVariantMap motifEntree(const char *cause, int index, const QString &id)
|
||||
@ -121,19 +131,21 @@ bool LoadConfigStore::load()
|
||||
// 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.
|
||||
// Chaque entrée est signalée, pas seulement la première : c'est la liste des réparations.
|
||||
// Les valeurs persistées sont citées ÉCHAPPÉES (citee(), ou `<<` sans noquote()) : avant Q7,
|
||||
// un client pouvait écrire un identifiant ou une échéance portant un saut de ligne.
|
||||
for (int i = 0; i < loaded.count(); ++i) {
|
||||
QString pourquoi;
|
||||
if (!validateLabels(LoadConfigs(QList<LoadConfig>{loaded.at(i)}), &pourquoi))
|
||||
qCWarning(dcNymeaEnergy()).noquote()
|
||||
qCWarning(dcNymeaEnergy())
|
||||
<< "[LoadConfigStore] CONFIGURATION CONSERVÉE, LIBELLÉ TROP LONG :" << pourquoi
|
||||
<< "— chargé tel quel ; toute écriture cliente sera refusée tant qu'il n'est pas raccourci.";
|
||||
if (!idIsWellFormed(loaded.at(i).id()))
|
||||
qCWarning(dcNymeaEnergy()).noquote()
|
||||
<< QStringLiteral("[LoadConfigStore] CONFIGURATION CONSERVÉE, IDENTIFIANT HORS RÈGLE : "
|
||||
"charge \"%1\" (%2 caractères ; attendu 1 à %3 caractères "
|
||||
"charge %1 (%2 caractères ; attendu 1 à %3 caractères "
|
||||
"[A-Za-z0-9._{}-]) — chargée telle quelle ; toute écriture cliente "
|
||||
"sera refusée tant qu'elle porte cet identifiant.")
|
||||
.arg(idPourMotif(loaded.at(i).id())).arg(loaded.at(i).id().toUcs4().size())
|
||||
.arg(citee(idPourMotif(loaded.at(i).id()))).arg(loaded.at(i).id().toUcs4().size())
|
||||
.arg(MaxIdChars);
|
||||
// L'échéance, elle, est RETIRÉE de la mémoire — l'entrée reste. Une échéance illisible
|
||||
// ne se résout en aucun instant ; la garder la ferait partir brute dans GetPlan, où
|
||||
@ -144,10 +156,10 @@ bool LoadConfigStore::load()
|
||||
if (!deadlineIsWellFormed(echeance)) {
|
||||
qCWarning(dcNymeaEnergy()).noquote()
|
||||
<< QStringLiteral("[LoadConfigStore] CONFIGURATION CONSERVÉE, ÉCHÉANCE ILLISIBLE : "
|
||||
"charge \"%1\", needs.dailyDeadline \"%2\" (attendu HH:MM, de 00:00 "
|
||||
"charge %1, needs.dailyDeadline %2 (attendu HH:MM, de 00:00 "
|
||||
"à 23:59) — IGNORÉE : ni l'arbitre ni l'optimiseur ne la reçoivent, "
|
||||
"l'obligation n'a plus d'échéance.")
|
||||
.arg(idPourMotif(loaded.at(i).id()), valeurPourMotif(echeance));
|
||||
.arg(citee(idPourMotif(loaded.at(i).id())), citee(valeurPourMotif(echeance)));
|
||||
LoadConfigNeeds besoins = loaded.at(i).needs();
|
||||
besoins.setDailyDeadline(QString());
|
||||
loaded[i].setNeeds(besoins);
|
||||
|
||||
@ -885,6 +885,78 @@ void TestLoadModel::testAppendConfigBoundsOnlyTheAddedEntry()
|
||||
{"length", 300}, {"max", 256}}));
|
||||
}
|
||||
|
||||
// ── relecture q7-suite — le journal ne recopie jamais une saisie telle quelle ──────────────────
|
||||
namespace {
|
||||
//! Ce qu'un client glisse dans un champ pour fabriquer une ligne de journal.
|
||||
const QString FORGE = QStringLiteral("x\n[NymeaEnergy] FAUX");
|
||||
|
||||
//! Capture les messages Qt UN PAR UN : une fausse ligne ne se voit qu'à l'intérieur d'un message.
|
||||
class Messages
|
||||
{
|
||||
public:
|
||||
Messages() { s_courant = this; s_origine = qInstallMessageHandler(&Messages::collecte); }
|
||||
~Messages() { qInstallMessageHandler(s_origine); s_courant = nullptr; }
|
||||
|
||||
bool contient(const QString &fragment) const
|
||||
{
|
||||
for (const QString &m : m_liste)
|
||||
if (m.contains(fragment))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
//! Les lignes qu'un message AJOUTE au journal en se faisant passer pour un message du moteur.
|
||||
QStringList faussesLignes() const
|
||||
{
|
||||
QStringList f;
|
||||
for (const QString &m : m_liste) {
|
||||
const QStringList lignes = m.split(QLatin1Char('\n'));
|
||||
for (int i = 1; i < lignes.count(); ++i)
|
||||
if (lignes.at(i).startsWith(QLatin1String("[NymeaEnergy]"), Qt::CaseInsensitive))
|
||||
f << lignes.at(i);
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
private:
|
||||
static void collecte(QtMsgType type, const QMessageLogContext &ctx, const QString &msg)
|
||||
{
|
||||
if (s_courant)
|
||||
s_courant->m_liste << msg;
|
||||
if (s_origine)
|
||||
s_origine(type, ctx, msg);
|
||||
}
|
||||
QStringList m_liste;
|
||||
static inline Messages *s_courant = nullptr;
|
||||
static inline QtMessageHandler s_origine = nullptr;
|
||||
};
|
||||
}
|
||||
|
||||
void TestLoadModel::testInheritedValuesAreEscapedInTheJournal()
|
||||
{
|
||||
// Avant Q7, un client pouvait persister un identifiant ou une échéance portant un saut de
|
||||
// ligne. load() les cite : échappés, ils ne fabriquent aucune fausse ligne de journal.
|
||||
const QString cfgPath = QDir::tempPath() + "/etm-loadcfg-journal.json";
|
||||
QFile::remove(cfgPath);
|
||||
qputenv("NYMEA_ENERGY_LOAD_CONFIG", cfgPath.toUtf8());
|
||||
const auto nettoyer = qScopeGuard([&] { QFile::remove(cfgPath); });
|
||||
|
||||
QVariantMap idForge = ballonMap();
|
||||
idForge.insert("id", FORGE);
|
||||
idForge.insert("label", QString(300, QLatin1Char('L'))); // LIBELLÉ TROP LONG cite l'identifiant
|
||||
QVariantMap echeanceForgee = routeurMap();
|
||||
echeanceForgee.insert("needs", QVariantMap{{"dailyDeadline", FORGE}, {"minEnergyWhPerDay", 1000}});
|
||||
ecrireFichier(cfgPath, {idForge, echeanceForgee});
|
||||
|
||||
Messages journal;
|
||||
LoadConfigStore relu;
|
||||
QCOMPARE(relu.configs().count(), 2);
|
||||
// Non vide : les trois avertissements sont partis, et ils citent la valeur ÉCHAPPÉE.
|
||||
QVERIFY(journal.contient(QStringLiteral("LIBELLÉ TROP LONG")));
|
||||
QVERIFY(journal.contient(QStringLiteral("IDENTIFIANT HORS RÈGLE : charge \"x\\n[NymeaEnergy] FAUX\"")));
|
||||
QVERIFY(journal.contient(QStringLiteral("needs.dailyDeadline \"x\\n[NymeaEnergy] …\"")));
|
||||
QVERIFY2(journal.faussesLignes().isEmpty(), qUtf8Printable(journal.faussesLignes().join(QStringLiteral(" | "))));
|
||||
}
|
||||
|
||||
void TestLoadModel::testRankOriginFollowsTheRankNotTheWrite()
|
||||
{
|
||||
// [LM-1209-c] La marque dit « ce rang est un défaut, pas un choix ». Ce qui la lève est
|
||||
|
||||
@ -94,6 +94,8 @@ private slots:
|
||||
void testLeftCharsNeverSplitsAPair();
|
||||
//! L'ajout par le moteur ne borne que l'entrée ajoutée — un hérité hors règle ne le bloque pas.
|
||||
void testAppendConfigBoundsOnlyTheAddedEntry();
|
||||
//! Relecture q7-suite : une valeur héritée citée par load() est échappée — aucune fausse ligne.
|
||||
void testInheritedValuesAreEscapedInTheJournal();
|
||||
|
||||
// ── 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