« Qu'est-ce que ce contrôle aurait attrapé s'il avait échoué ? » passée à la suite existante. Le résultat n'est pas une liste de tests à supprimer. Contrôles qui ne contrôlent rien : les assertions conditionnelles dans une boucle sont à zéro — la seule qui existait a été corrigée avant-hier. Sur dix identités auto-référentielles relevées, UNE SEULE est vaine : authorisedW − committedW == remainingW relisait une affectation d'une ligne. Elle est retirée AVEC SA RAISON à sa place, et avec la distinction d'avec l'identité du budget — qui croise remainingSurplusW filé le long de la cascade et allocatedW accumulé en trois sites distincts, donc attrape un site qui décrémenterait sans accumuler. Supprimer sans dire pourquoi aurait invité quelqu'un à la remettre. La moitié qui compte : l'index annonçait 47 règles sans porteur. Il mesurait la CITATION, pas la couverture — douze étaient couvertes par un test qui ne les nommait pas. Citées par un test : 22 → 40. Les liens sont maintenant vérifiables par la machine au lieu de reposer sur la mémoire de qui a écrit quoi ; c'est la doctrine du dépôt appliquée à elle-même. Sur les 31 restantes, 23 relèvent de familles non implantées — écrire leur test poserait un invariant sur du code que rien n'émet. Six sont du cadrage mal classé. Et UNE manque vraiment : LM-1210-a, la pire phase faute de masque, qu'aucun test n'exerce parce que le mock ne sait pas produire une triphasée déséquilibrée. Même angle mort que l'inventaire, nommé deux fois. tools/audit-controles.py pérennise les deux motifs. Il ne remplace pas la question, il la pose là où elle se pose souvent — et il ne juge pas : la lecture tranche, l'écart entre les dix relevées et la seule vaine le montre. Le résultat le plus utile n'est pas le défaut trouvé, c'est le chiffre corrigé. On croyait 47 règles non couvertes ; il y en avait 31, dont 23 légitimes et une seule qui manque. Savoir lesquelles change ce qu'on surveille. Suite : simulation 62/62, 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
66 lines
3.5 KiB
Python
66 lines
3.5 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 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))
|