Patrick Schurig d4b7d4dced test(décisions): référence au bit près des scénarios de simulation (brief 4 T0.5)
Preuve « sans endpoint = identique au bit près », posée AVANT tout refactoring.

`nymea-energy-decisions` est la suite `simulation` compilée à l'identique, plus un
enregistreur branché à l'édition de liens (`--wrap`) : aucune ligne de la suite ni du
moteur n'est modifiée. Chaque cycle (`simulationCallUpdate`) écrit une ligne NDJSON :
télémétrie de l'arbitre (actions du créneau dans l'ordre, motifs, niveaux, budget, soutirage,
consigne posée), actions laissées au proxy, état des Things. Les scénarios qui interrogent
un `RuleBasedScheduler` local voient leur `Plan` sérialisé champ par champ.

Exclus : `command.since` et les compteurs `totalEnergy*` du mock (horloge murale), les
ThingId (alias par rang dans le créneau, à chaque ligne : deux bornes identiques
reçoivent leur rang provisionné dans l'ordre d'un QHash). `planId` n'est pas lu.

Référence générée sur 83483cc (1.15.2+etm69) : 66 fonctions, 23 673 cycles + 36 plans
directs, deux exécutions identiques à l'octet. Chaque processus a un /tmp privé : libnymea
range l'état de test sous /tmp/nymea-test, partagé et cumulatif (348 bornes orphelines
trouvées dans energy-load-configuration.json).

  tests/auto/decisions/run-decisions.sh <build> check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 07:24:00 +02:00

91 lines
4.0 KiB
Bash
Executable File

#!/bin/bash
# Décisions de référence (brief 4 T0.5) — rejoue les scénarios de `simulation` et compare, cycle
# par cycle, ce que l'arbitre a décidé au fichier de référence commité.
#
# run-decisions.sh <build> record <sortie.ndjson> enregistre seulement
# run-decisions.sh <build> check enregistre puis compare à la référence
# (code 1 au premier écart, et il est montré)
#
# <build> : arbre construit depuis le .pro racine avec CONFIG+=build_tests (le binaire
# `nymea-energy-decisions` y est sous tests/auto/decisions/).
#
# AGENTS.md : UN PROCESSUS PAR TEST, chacun sous MemoryMax=2G. Les tests ne tournent jamais en
# parallèle (le harnais nymea ouvre des ports fixes). Un test qui sort en erreur est rejoué UNE
# fois — instabilité connue du port 4847 — et le rejeu est annoncé, jamais masqué.
set -u
ICI="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REFERENCE="$ICI/reference/decisions.ndjson.xz"
BUILD="$(cd "${1:?build}" && pwd)"
MODE="${2:?record|check}"
BIN_DIR="$BUILD/tests/auto/decisions"
BIN="$BIN_DIR/nymea-energy-decisions"
[ -x "$BIN" ] || { echo "binaire absent : $BIN" >&2; exit 2; }
case "$MODE" in
record) SORTIE="${3:?sortie}" ;;
check) SORTIE="$(mktemp --suffix=.ndjson)" ;;
*) echo "mode inconnu : $MODE" >&2; exit 2 ;;
esac
TRAVAIL="$(mktemp -d)"
trap 'rm -rf "$TRAVAIL"' EXIT
export NYMEA_PLUGINS_PATH="$BUILD/tests/mocks/plugins/energymocks:/usr/lib/x86_64-linux-gnu/nymea/plugins"
# UN /tmp PRIVÉ ET VIDE PAR PROCESSUS. libnymea range l'état du serveur de test sous
# `/tmp/nymea-test`, en dur : configuration des charges, energy.conf, magasin de prévision. Ce
# répertoire est partagé par toutes les exécutions de la machine et il s'accumule — mesuré le
# 2026-10-16, un energy-load-configuration.json de 348 bornes orphelines laissé là le 14. Sans
# isolation, le résultat dépendrait de l'historique de la machine, pas du code.
lancer() { # $1 = fonction, $2 = fichier NDJSON du test
rm -f "$2"
( cd "$BIN_DIR" && ETM_DECISIONS_OUT="$2" \
systemd-run --user --scope -q -p MemoryMax=2G -p MemorySwapMax=0 \
bwrap --dev-bind / / --tmpfs /tmp --bind "$BUILD" "$BUILD" --bind "$TRAVAIL" "$TRAVAIL" \
--chdir "$BIN_DIR" \
./target_wrapper.sh ./nymea-energy-decisions "$1" > "$TRAVAIL/$1.log" 2>&1 )
}
mapfile -t FONCTIONS < <(cd "$BIN_DIR" && ./nymea-energy-decisions -functions 2>/dev/null \
| sed -n 's/^\([A-Za-z_][A-Za-z0-9_]*\)()$/\1/p')
[ "${#FONCTIONS[@]}" -gt 0 ] || { echo "aucune fonction listée" >&2; exit 2; }
: > "$SORTIE"
total=0
for f in "${FONCTIONS[@]}"; do
out="$TRAVAIL/$f.ndjson"
lancer "$f" "$out"; code=$?
if [ "$code" -ne 0 ]; then
echo "REJEU $f (code $code) — $(grep -m1 -E 'FAIL!' "$TRAVAIL/$f.log" | cut -c1-160)" >&2
lancer "$f" "$out"; code=$?
fi
n=0; [ -f "$out" ] && { n=$(wc -l < "$out"); cat "$out" >> "$SORTIE"; }
total=$((total + n))
resume=$(grep -m1 '^Totals:' "$TRAVAIL/$f.log")
printf '{"fn":"%s","fin":true,"exit":%d,"cycles":%d}\n' "$f" "$code" "$n" >> "$SORTIE"
printf '%-48s exit=%d cycles=%-6d %s\n' "$f" "$code" "$n" "$resume" >&2
done
# Le contrôle ne doit pas pouvoir disparaître : un enregistreur débranché (wrap perdu, binaire
# reconstruit sans lui) produirait un fichier sans cycle, que la comparaison verrait comme un
# écart massif — mais on le dit ici en clair.
[ "$total" -gt 0 ] || { echo "AUCUN cycle enregistré — l'enregistreur n'est pas branché" >&2; exit 2; }
echo "fonctions=${#FONCTIONS[@]} cycles=$total" >&2
[ "$MODE" = record ] && exit 0
ATTENDU="$TRAVAIL/attendu.ndjson"
xz -dc "$REFERENCE" > "$ATTENDU" || exit 2
if cmp -s "$ATTENDU" "$SORTIE"; then
echo "IDENTIQUE à la référence ($(wc -l < "$ATTENDU") lignes)" >&2
rm -f "$SORTIE"
exit 0
fi
echo "ÉCART avec la référence — premières lignes divergentes (- attendu, + obtenu) :" >&2
diff "$ATTENDU" "$SORTIE" | head -c 6000 >&2
echo >&2
echo "fichier obtenu conservé : $SORTIE" >&2
exit 1