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