Le premier dit ce que le MATÉRIEL permet, le second ce que le MOTEUR fait. Seul l'EvAdapter remplissait sessionMeasurable, donc sur toute charge non-VE le premier disait oui et le second non — et l'avertissement de saisie de l'app, qui se déclenche sur progressMeasurable, restait muet exactement sur les charges achetées à l'aveugle. sessionWh est alimenté depuis le compteur désigné. L'avertissement de l'app est arrivé AVANT que j'écrive la garde, et il était décisif. Un compteur qui disparaît peut publier 0 au lieu de se taire : la V2C a forgé un zéro sur currentPower et ses booléens, mais laissé sessionEnergy à sa dernière valeur. La politique dépend du plugin — donc inutilisable comme garde. Et le point à vérifier avait une réponse nette : il n'existe AUCUN signal de joignabilité fiable. setupComplete parle du setup et non du lien ; connected est maintenu par le plugin, la V2C basculant après trois échecs quand un compteur générique peut ne jamais basculer. La garde ne pouvait pas reposer là-dessus. Elle porte donc sur la SÉMANTIQUE du compteur, seule chose que le moteur sache sans parier : sur un compteur de session un recul est une session neuve, sur un compteur de vie c'est une anomalie — on gèle, on ne recapture jamais. Sans elle, le scénario chiffré sort tel quel : base reprise à 0 pendant l'absence, retour avec 10 kWh réels, deliveredWh à 10 000 en un cycle, obligation déclarée tenue sans rien de livré. Et R6 ne l'attrape pas : elle n'interdit ECO_FLOOR_MET que sous unmeasurable, alors que ce chemin est MESURÉ — c'est le seul par lequel un faux « tenue » peut sortir, et c'est celui qu'ouvrait l'alimentation depuis un compteur. audit-controles.py dit maintenant en tête qu'il ne tranche pas et ne doit jamais trancher : neuf des dix identités relevées contrôlaient réellement, et un verdict automatique aurait supprimé neuf contrôles utiles. AGENTS.md : la diversité du mock n'est pas un confort de test, c'est une borne supérieure sur ce que la suite peut prouver — elle se fixe en écrivant les classes, pas les tests. Quatrième occurrence du piège d'insertion doxygen, rattrapée par le même contrôle. Comme annoncé. Suite : simulation 63/63, loadmodel 22/22, charging 48/48 identique à la référence, spotmarket 32/32, doxygen 0 avertissement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
76 lines
4.0 KiB
Python
76 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
||
"""Cherche les CONTRÔLES QUI NE CONTRÔLENT RIEN.
|
||
|
||
La question, posée à chaque assertion : **qu'aurait-elle attrapé si elle avait échoué ?**
|
||
S'il n'y a pas de réponse, elle ne contrôle rien — et le voyant vert ment.
|
||
|
||
Deux motifs, tous deux rencontrés en vrai :
|
||
|
||
1. ASSERTION CONDITIONNELLE DANS UNE BOUCLE. Si la condition n'est jamais vraie, l'assertion
|
||
ne s'exécute pas et le test passe. Vu le 2026-09-01 : « sans mesure, pas de `command` »
|
||
n'avait AUCUNE charge à examiner, et la garde retirée le test passait toujours.
|
||
Parade : compter les cas examinés, échouer si le compte est nul.
|
||
|
||
2. IDENTITÉ QUI SE REFERME SUR ELLE-MÊME. Quand les deux membres viennent de la même source,
|
||
l'identité est vraie par construction et rassure là où rien ne protège. Vu le 2026-09-03 :
|
||
`authorisedW − committedW == remainingW` relisait l'affectation d'une seule ligne.
|
||
|
||
⚠️ Toutes les identités auto-référentielles ne sont pas vaines : celle du BUDGET croise
|
||
`remainingSurplusW`, filé le long de la cascade, avec `allocatedW` accumulé en trois sites
|
||
distincts. Le motif signale, il ne juge pas — c'est à la lecture de trancher.
|
||
|
||
⚠️ CET OUTIL NE TRANCHE PAS, ET NE DOIT JAMAIS TRANCHER.
|
||
|
||
Il SIGNALE un motif ; c'est la LECTURE qui décide. Au balayage du 2026-09-03, il a relevé dix
|
||
identités auto-référentielles : NEUF contrôlaient réellement quelque chose, une seule était vaine.
|
||
Un outil qui aurait rendu un verdict aurait supprimé neuf contrôles utiles.
|
||
|
||
Le prochain qui lira ce fichier sera tenté d'automatiser le verdict. Ne le fais pas : la question
|
||
« qu'aurait-il attrapé s'il avait échoué ? » demande de savoir d'où viennent les deux membres, et
|
||
cette connaissance n'est pas dans le texte du test.
|
||
|
||
Cet outil ne remplace pas la question, il la pose là où elle se pose souvent.
|
||
"""
|
||
import re
|
||
from pathlib import Path
|
||
ASSERT = r'(?:QCOMPARE|QVERIFY2?|QTRY_COMPARE|QTRY_VERIFY2?)'
|
||
|
||
def fonctions(txt):
|
||
for m in re.finditer(r'\nvoid\s+\w+::(\w+)\(\)\s*\n\{', txt):
|
||
deb = m.end(); prof, i = 1, deb
|
||
while i < len(txt) and prof:
|
||
if txt[i] == '{': prof += 1
|
||
elif txt[i] == '}': prof -= 1
|
||
i += 1
|
||
yield m.group(1), txt[deb:i], txt[:m.start()].count('\n')+2
|
||
|
||
cond, miroir = [], []
|
||
for f in Path('tests/auto').rglob('*.cpp'):
|
||
if 'mock' in str(f) or 'experience' in str(f): continue
|
||
txt = f.read_text(encoding='utf-8')
|
||
for nom, corps, ligne in fonctions(txt):
|
||
if nom.endswith('_data'): continue
|
||
# MOTIF 1 — assertion sous un `if` DANS une boucle : si la condition n'est jamais
|
||
# vraie, l'assertion ne s'exécute pas et le test passe sans rien vérifier.
|
||
for m in re.finditer(r'for\s*\([^)]*\)\s*\{(.{0,900}?)\n\s*\}', corps, re.S):
|
||
bloc = m.group(1)
|
||
if re.search(r'\n\s*if\s*\([^)]*\)[^;{]*\n?\s*' + ASSERT, bloc):
|
||
garde = re.search(r'\b(examin|vus|compte|trouv)\w*\b', corps, re.I)
|
||
cond.append((f, ligne, nom, "gardé" if garde else "NON GARDÉ"))
|
||
break
|
||
# MOTIF 2 — comparaison dont les deux membres partent de la MÊME racine.
|
||
for m in re.finditer(r'QCOMPARE\(\s*([^,]+?)\s*,\s*([^;]+?)\s*\)\s*;', corps):
|
||
a, b = m.group(1), m.group(2)
|
||
ra = re.match(r'\s*(?:qRound\()?\s*([A-Za-z_][\w]*)', a)
|
||
rb = re.match(r'\s*(?:qRound\()?\s*([A-Za-z_][\w]*)', b)
|
||
if ra and rb and ra.group(1) == rb.group(1) and '.' in a and '.' in b:
|
||
miroir.append((f, ligne, nom, m.group(0)[:96]))
|
||
|
||
print("=== MOTIF 1 — assertion conditionnelle dans une boucle : %d ===" % len(cond))
|
||
print(" Si la condition n'est jamais vraie, rien ne s'exécute et le test PASSE.")
|
||
for f, l, n, g in cond: print(" [%s] %s:%d %s" % (g, f, l, n))
|
||
print()
|
||
print("=== MOTIF 2 — les deux membres viennent de la MÊME racine : %d ===" % len(miroir))
|
||
print(" L'identité se referme sur elle-même : vraie par construction.")
|
||
for f, l, n, t in miroir: print(" %s:%d %s\n %s" % (f, l, n, t))
|