etm-powersync-energy-plugin.../tools/check-simulation-clock.py
Patrick Schurig 0d2b6728ae [upstream-fix] un verdict de test ne doit pas dépendre de l'heure
Six appels à `update(QDateTime::currentDateTime())` manquaient la garde
ENERGY_SIMULATION que leurs voisins ont (l.124, l.288) :
setPhasePowerLimit, setAcquisitionTolerance, et trois dans
onActionExecuted — qui partent sur toute action manuelle appliquée à une
borne.

Un cycle d'arbitrage complet partait donc à l'horloge MURALE au milieu
d'une suite qui injecte un temps simulé. Les deux horloges se mélangeaient
dans les verrous : l'armement paresseux (ECS-412) figeait m_lastSwitch sur
l'heure qu'il était, et un scénario daté en dur se déroulait « avant » son
propre armement — elapsed négatif, verrou jamais ouvert.

Ce n'est pas un défaut de production : il n'y a là-bas qu'une seule horloge.
C'est une faille de REPRODUCTIBILITÉ, et elle touchait tout test de
simulation réglant une limite de phase, une tolérance d'acquisition, ou
exécutant une action manuelle sur une borne.

PREUVE DU CORRECTIF : la garde rendue, testL4ShedsAPacAndSaysItForcedTheLock
repasse avec une DATE FIXE, trois fois sur trois — ce qu'il ne pouvait pas
faire. L'ancrage sur l'horloge réelle, posé la veille faute d'avoir la
cause, était un contournement ; il est retiré, et la date fixe revient parce
qu'elle rend les journaux reproductibles.

tools/check-simulation-clock.py, câblé en 4/4 de ci-quality.sh, et vérifié
mordant : garde retirée, il nomme la ligne exacte. Il a trouvé trois des six
déclencheurs dès son premier passage.

Pourquoi une vérification STATIQUE et pas un test : la suite ne peut pas
dire d'elle-même que son verdict dépend de l'heure, puisque c'est le verdict
qui ment. Le défaut n'a été trouvé que parce qu'il échouait cinq fois sur
cinq à code identique — une fenêtre de nuisance plus étroite ne basculerait
qu'une fois sur vingt, passerait pour une fragilité de temporisation, et
serait relancée jusqu'à ce qu'elle passe. C'est la famille de la semaine
sous une forme de plus : après l'instrument dont le silence ressemble à un
résultat, l'instrument dont le VERDICT ment selon l'heure.

Suite complète : simulation 54/54, charging 48/48 identique à la référence
ligne à ligne, loadmodel 21/21, spotmarket 32/32, doxygen 0 avertissement.
Aucun test ne dépendait de ces cycles.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F7G5VeaPVSMeVNjiGj36p
2026-08-31 00:10:55 +02:00

83 lines
3.4 KiB
Python
Executable File

#!/usr/bin/env python3
"""Refuse un déclencheur d'arbitrage câblé sur l'HORLOGE MURALE hors garde de simulation.
Pourquoi cette vérification existe
----------------------------------
`setPhasePowerLimit()` lançait un cycle d'arbitrage complet avec
`QDateTime::currentDateTime()`, sans la garde `ENERGY_SIMULATION` que ses voisins ont. Dans une
suite qui injecte un temps SIMULÉ, les deux horloges se mélangent alors dans les verrous :
l'armement paresseux (ECS-412) fige `m_lastSwitch` sur l'heure qu'il est, et un scénario daté en
dur se déroule « avant » son propre armement.
**Le test passait ou échouait selon l'heure de la journée.** Il n'a été trouvé que parce qu'il
échouait cinq fois sur cinq — un défaut qui ne bascule qu'une fois sur vingt passerait pour une
fragilité de temporisation, et serait relancé jusqu'à ce qu'il passe. C'est un instrument dont le
verdict ment selon l'heure, et rien dans la suite ne peut le dire d'elle-même : la vérification
doit donc être STATIQUE.
"""
import re
import sys
from pathlib import Path
RACINE = Path(__file__).resolve().parent.parent
# Fonctions qui entraînent un CYCLE complet. En appeler une avec l'horloge murale est un
# déclencheur réel, légitime en production et interdit en simulation.
DECLENCHEURS = re.compile(r'\b(update|runSurplusPlanning|verifyOverloadProtection)\s*\(\s*QDateTime::currentDateTime\(\)')
FICHIERS = list((RACINE / 'energyplugin').rglob('*.cpp'))
def analyser(chemin):
"""Retourne les lignes fautives : un déclencheur hors de tout `#ifndef ENERGY_SIMULATION`."""
fautes = []
# Pile des conditions de préprocesseur : True quand on est dans une branche qui EXCLUT la
# simulation. Le suivi est volontairement simple — il ne gère que ce que ce dépôt écrit.
pile = []
for numero, ligne in enumerate(chemin.read_text(encoding='utf-8').splitlines(), 1):
nu = ligne.strip()
if nu.startswith('#ifndef ENERGY_SIMULATION'):
pile.append(True)
continue
if nu.startswith('#ifdef ENERGY_SIMULATION'):
pile.append(False)
continue
if nu.startswith('#if'):
pile.append(False)
continue
if nu.startswith('#else') and pile:
pile[-1] = not pile[-1]
continue
if nu.startswith('#endif') and pile:
pile.pop()
continue
if nu.startswith('//'):
continue
if DECLENCHEURS.search(ligne) and not any(pile):
fautes.append((numero, nu))
return fautes
def principal():
total = []
for f in sorted(FICHIERS):
for numero, texte in analyser(f):
total.append((f.relative_to(RACINE), numero, texte))
if not total:
print("✓ aucun déclencheur d'arbitrage à l'horloge murale hors garde de simulation")
return 0
print("✗ %d déclencheur(s) d'arbitrage câblé(s) sur l'HORLOGE MURALE sans garde :" % len(total))
for chemin, numero, texte in total:
print(" %s:%d %s" % (chemin, numero, texte))
print()
print(" Un cycle à l'horloge murale dans une suite à temps simulé mélange les deux horloges")
print(" dans les verrous : le test passe ou échoue SELON L'HEURE DE LA JOURNÉE.")
print(" → réparer : entourer l'appel de #ifndef ENERGY_SIMULATION / #endif, comme ses voisins.")
return 1
if __name__ == '__main__':
sys.exit(principal())