Patrick Schurig e7485e76f9 audit: la question passée à toute la suite — ce qu'on croyait vérifié
« 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
2026-09-03 06:58:42 +02:00

66 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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))