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>
202 lines
11 KiB
C++
202 lines
11 KiB
C++
// SPDX-License-Identifier: GPL-3.0-or-later
|
||
#ifndef TESTLOADMODEL_H
|
||
#define TESTLOADMODEL_H
|
||
|
||
#include <QObject>
|
||
|
||
/*!
|
||
* \brief Logique pure du modèle de charge : table de paliers et identité matérielle.
|
||
*
|
||
* Aucun serveur nymea ici. Ce qui est éprouvé ne dépend ni d'un ThingManager ni d'un
|
||
* cycle d'arbitrage : la dérivation des paliers est une combinatoire, et
|
||
* \c EnergyArbitrator::sameHardware() une comparaison.
|
||
*/
|
||
class TestLoadModel : public QObject
|
||
{
|
||
Q_OBJECT
|
||
|
||
private slots:
|
||
//! La table dérive de TOUTES les sommes de sous-ensembles, triées, 0 compris.
|
||
void testStageTableIsDerivedFromSubsetSums();
|
||
|
||
//! Puissances distinctes : l'ordre de déclaration n'a aucun effet observable.
|
||
void testRelayOrderIsInertWhenPowersAreDistinct();
|
||
|
||
//! Puissances égales : l'ordre décide QUEL contact sert le palier — et ça se voit.
|
||
void testRelayOrderDecidesWhichContactServesATiedStage();
|
||
|
||
//! Un réordonnancement sans effet ne doit PAS coûter une reconstruction.
|
||
void testReorderingDistinctRelaysKeepsTheAdapter();
|
||
|
||
//! Mais un échange de contacts à puissance égale, si.
|
||
void testSwappingTiedRelaysRebuilds();
|
||
|
||
//! Libellé, rang et besoins se mettent à jour en place : jamais de reconstruction.
|
||
void testSoftFieldsNeverRebuild();
|
||
|
||
// ── §11 — compteur et sonde par charge ─────────────────────────────────
|
||
//! Les deux champs traversent l'aller-retour, et sont OMIS quand ils sont vides.
|
||
void testMeasurementThingsRoundTrip();
|
||
|
||
//! Ils ne sont pas des canaux de commande : ils n'entrent pas dans claimedThingIds().
|
||
void testMeasurementThingsAreNotClaimed();
|
||
|
||
//! Donc deux charges peuvent partager le même compteur et la même sonde.
|
||
void testTwoLoadsMayShareAMeter();
|
||
|
||
//! Et les changer ne reconstruit rien : ils n'ont aucun effet sur le matériel.
|
||
void testMeasurementThingsAreSoftFields();
|
||
|
||
// ── §12 — le plancher de modulation ────────────────────────────────────
|
||
//! Déclaré en dynamic, et il traverse l'aller-retour.
|
||
void testFloorIsDeclaredForContinuousLoads();
|
||
|
||
//! Interdit là où il se DÉRIVE : deux sources d'une même vérité divergent.
|
||
void testFloorIsRefusedWhereItWouldBeDerived();
|
||
|
||
//! Un plancher au-dessus du plafond rendrait la charge inatteignable.
|
||
void testFloorAboveCeilingIsRefused();
|
||
|
||
// ── 3g-2 — le rang de la borne vit dans LoadConfig (LM-1206/LM-1207) ───
|
||
//! Une entrée evcharger est valide SANS charge utile, et traverse l'aller-retour.
|
||
void testEvChargerConfigIsRankOnly();
|
||
|
||
//! Toute charge utile d'un autre mécanisme y est refusée (LM-300, union discriminée).
|
||
void testEvChargerConfigRefusesPayloads();
|
||
|
||
//! Elle revendique son Thing : une borne ne peut pas être aussi une charge pilotée.
|
||
void testEvChargerClaimsItsThing();
|
||
|
||
// ── §12 / LM-1209 — l'auto-provisionnement remplit un trou, il ne décide pas ───
|
||
//! L'unicité de rang REFUSE une écriture, mais n'écarte JAMAIS au chargement : les deux
|
||
//! vérifications sont séparées parce que leurs gestes le sont (LM-1209-a).
|
||
void testRankUniquenessRefusesWriteButNeverDiscards();
|
||
|
||
//! 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();
|
||
// Q7, suite — le refus de SetLoadConfig publie sa cause ; id et échéance bornés à l'écriture
|
||
//! Chaque cause de refus : son motif (cause et endroit), et rien de modifié, ni en mémoire
|
||
//! ni sur disque.
|
||
void testLoadConfigRefusalNamesItsCause_data();
|
||
void testLoadConfigRefusalNamesItsCause();
|
||
//! Le motif se rend en français, avec l'endroit et sans le libellé.
|
||
void testLoadConfigRefusedIsRendered();
|
||
//! id : 128 caractères de [A-Za-z0-9._{}-] à l'écriture ; une config héritée hors règle est
|
||
//! chargée telle quelle et signalée.
|
||
void testIdIsBoundedOnWriteNeverOnLoad();
|
||
//! dailyDeadline : vide ou HH:MM à l'écriture ; héritée illisible, l'entrée est chargée et
|
||
//! l'échéance retirée de la mémoire.
|
||
void testDailyDeadlineIsCheckedOnWriteAndDroppedOnLoad();
|
||
//! Couper en caractères ne coupe jamais une paire de substitution.
|
||
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();
|
||
//! 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 ───────────────────────
|
||
//! On compare des MARGES, jamais des plafonds : le plus contraignant mord, et on sait
|
||
//! lequel. Aucun plafond déclaré ≠ marge nulle. Départage stable à marge égale.
|
||
void testEcoSpreadHardensAndSaysSo();
|
||
void testEcoUnmeasurableBuysAtAConstantRate();
|
||
void testMinimumIsServedEvenWhenTheObligationIsMet();
|
||
void testStoredInstantsSurviveATimeZoneChange();
|
||
void testReportingDayBoundaryIsPublishedNotDeducible();
|
||
void testDrawMarginComparesHeadroomsNotCaps();
|
||
|
||
//! Une révision n'existe que si la PRÉVISION a changé — et le contenu seul en décide.
|
||
void testTheContentIsTheRevisionIdentity();
|
||
//! La courbe se relit telle qu'elle était connue, et ses trous restent des trous.
|
||
void testTheCurveIsReproducibleAndKeepsItsHoles();
|
||
//! La rétention garde les DEUX bouts : n'en garder qu'un détruirait la révision.
|
||
void testRetentionKeepsBothEndsOfEachRevision();
|
||
//! La courbe COURANTE est la dernière VUE, pas la dernière apparue.
|
||
void testTheCurrentCurveIsTheLastSeenNotTheLastNew();
|
||
//! La charge utile se REFUSE plutôt que de s'interpréter au mieux — mais pas pour un attribut.
|
||
void testAPayloadIsRefusedOnFormNeverOnAMissingAttribute();
|
||
void testTransportV2IsReadAndAnUnknownCorrectionRefused();
|
||
void testTheSiteCurveIsClippedAfterTheSum();
|
||
|
||
//! Brief 3 T2 — une ligne par pas ÉCOULÉ et par plan ; v1 laisse gti/tempC/paramètres v2 NULL.
|
||
void testTheLearningLogWritesOneRowPerElapsedStepAndPlan();
|
||
//! Mesure, écrêtage, neige : LUS ou NULL, jamais devinés.
|
||
void testTheLearningLogReadsMeasureCurtailmentAndSnow();
|
||
//! « J−1 18:00 locale » et H−1 : la révision du site en vigueur, par sa clé de contenu.
|
||
void testTheLearningLogPointsAtTheRevisionsInForce();
|
||
//! Base abîmée : table refaite, journalisé, le service continue ; rétention 400 jours.
|
||
void testTheLearningLogSurvivesADamagedTable();
|
||
//! L'écriture ne bloque jamais l'appelant, même base verrouillée.
|
||
void testTheLearningLogNeverBlocksTheCycle();
|
||
//! etm68 — une table etm67 RÉELLE est migrée en place : lignes conservées, colonnes NULL.
|
||
void testTheLearningLogMigratesAnEtm67TableInPlace();
|
||
//! etm68 — le GTI observé va au pas qu'il moyenne, complète seulement ses colonnes, si NULL.
|
||
void testTheLearningLogAttachesTheObservedGtiToItsStep();
|
||
|
||
// ── Brief 3 T4 — tarif saisi ───────────────────────────────────────────
|
||
//! Validation STRICTE : prix ≥ 0, plages lisibles, 24 h sans trou ni chevauchement — refus codés.
|
||
void testTheTariffIsValidatedStrictly();
|
||
//! Les plages sont des heures MURALES : jours de 23 h et de 25 h, oracle hors Qt.
|
||
void testTheTariffFollowsLocalTimeAcrossDstDays();
|
||
//! Un créneau à cheval sur deux zones reçoit la moyenne pondérée par la durée.
|
||
void testAStraddlingSlotGetsTheTimeWeightedMean();
|
||
|
||
// ── Brief 3 T4 — consommation de base ──────────────────────────────────
|
||
//! La minute retranche chaque charge selon sa source ; commande inconnue → minute écartée.
|
||
void testTheBaseloadMinuteSubtractsEachLoadBySource();
|
||
//! Profil = médiane par créneau local et type de jour sur 28 jours ; qualité dite ; 23 h / 25 h.
|
||
void testTheBaseloadProfileIsTheMedianPerSlotAndDayType();
|
||
//! Sous 7 jours couverts (à 90 %) : `none`, et rien d'inventé.
|
||
void testTheBaseloadIsNoneUnderSevenDays();
|
||
//! Tables dédiées, écriture asynchrone, recalcul, rétention — sans toucher forecastLearning.
|
||
void testTheBaseloadLogWritesWithoutTouchingTheOtherTables();
|
||
|
||
// ── Brief 4 T1 — sérialiseur GetPlan (OPTIMIZER_PROTOCOL 1.1 §5) ───────
|
||
// Corps dans testoptimizerprotocol.cpp ; références dans data/optimizer/.
|
||
//! Golden : site sans PV — grille, base sans profil et tarif saisi, aucune courbe.
|
||
void testGetPlanParamsGoldenSiteSansPv();
|
||
//! Golden : PV + ECS relay-router + borne + SG-Ready, batterie, dérogation à cible.
|
||
void testGetPlanParamsGoldenPvEcsBorneSgReady();
|
||
//! Golden : prévision absente (motif) → `pv: null` ; sans instantané → `forecast: null`.
|
||
void testGetPlanParamsGoldenPrevisionAbsente();
|
||
//! Golden : charge `available=false` figée, et borne configurée sans appareil.
|
||
void testGetPlanParamsGoldenChargeIndisponibleFigee();
|
||
//! Inconnu = null ou absent, jamais 0 ; ni `group`, ni champs réservés, ni `force`.
|
||
void testGetPlanParamsNeverInventsAValue();
|
||
//! `meter.exportW` est la mesure brute (règle 8).
|
||
void testGetPlanParamsMeterIsRaw();
|
||
//! Wh par pas → puissance moyenne ; une case sans point reste null.
|
||
void testForecastWhPerStepBecomesMeanPower();
|
||
//! Un contexte en HEURE LOCALE (celle de currentDateTime()) émet des instants AVEC décalage,
|
||
//! sous plusieurs fuseaux, été comme hiver (constat VM du 2026-10-21 : -32602 à chaque cycle).
|
||
void testGetPlanParamsInstantsCarryTheirOffset_data();
|
||
void testGetPlanParamsInstantsCarryTheirOffset();
|
||
//! Nuit du changement d'heure : 02:30 ambiguë (hiver) et inexistante (été) → instants exacts.
|
||
void testGetPlanParamsInstantsAcrossTheDstChange();
|
||
|
||
// ── Brief 4 T5 — catalogue des motifs (corps dans testdecisioncatalogue.cpp) ──
|
||
//! Anti-dérive : knownCodes() == les constantes de l'en-tête, chacune avec un rendu connu.
|
||
void testDecisionCodesListIsComplete();
|
||
//! Liste blanche de l'optimiseur (E14) : ni sécurité, ni verrou, ni état du moteur.
|
||
void testOptimizerMayEmitOnlyWhitelistedCodes();
|
||
//! ECO_FLOOR_BELOW_STAGE se rend « rien n'est acheté », avec ou sans obligation en énergie.
|
||
void testEcoFloorBelowStageIsRenderedAsNoPurchase();
|
||
|
||
// Brief 4 T3 — plan d'un optimiseur (OptimizerPlan, fonctions pures)
|
||
void testOptimizerParamKeysMatchInterface();
|
||
void testOptimizerPlanIsConverted();
|
||
void testOptimizerPlanVerdicts_data();
|
||
void testOptimizerPlanVerdicts();
|
||
void testOptimizerReasonIsFiltered();
|
||
void testOptimizerForceAndReservedAreIgnored();
|
||
void testOptimizerUnavailableLoadKeepsTheShadow();
|
||
void testOptimizerFinaliseUsesTheAppliedAction();
|
||
|
||
};
|
||
|
||
#endif // TESTLOADMODEL_H
|