Files
goodwe-addon/.gitea/ci/run-tests.sh
T
glenn schrooyenandClaude 40595e42ae SAFETY-126: a landing error on the aim is not an overrun
During the night grid charge under the peak reserve the guard alarmed
(ERROR PEAK_GUARD_FLOOR_LIMITED, "Battery stopped limiting the peak") and the
Batteries "Why" said "The peak limit needs power" at quarter ends, while every
quarter landed on the aim. The charge lands the period 0-7 W over the aim and
excess_w()'s 900 / remaining turns that into tens of kW in the last second;
with nothing able to give, check_peak_floor_limited() alarmed on any
wanted > 0.

Each cycle the guard wants power, _peak_landing() asks misses_aim(): with the
batteries at 0 W, does the period end more than ALARM_MISS_W over the aim? If
not, the decision is within_aim: no reserve alarm, no red peak row, no "Why"
peak. The claim and the commands are unchanged. A real overrun under the
reserve alarms on the same cycle as before.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-10-31 01:26:02 +01:00

126 lines
5.5 KiB
Bash
Executable File

#!/bin/sh
# CI-15: run the add-on's test_*.py files, in parallel, inside the CI container.
# Called by the step "the add-on's own tests" of .gitea/workflows/checks.yml as
# sh -eu /w/goodwe-addon/.gitea/ci/run-tests.sh
# and runnable the same way from a clone (see TESTS_DIR below).
#
# WHY: the serial loop ran past the runner's 60-minute limit (SAFETY-125: 85 files,
# 3 557 s of test time, run 1430 cut off at file 84). The host has 12 cores.
#
# SHAPE
# 1. A parallel batch: JOBS files at a time (xargs -P), longest first.
# 2. THEN the serial group, one at a time, after the batch has finished, so
# each runs on a machine with nothing else of ours on it.
# Every file writes to its own $RUN_DIR/<file>.out (there is no shared output
# file) and prints its result as ONE block under a lock, so the PASS/FAIL
# lines and the `CI: ` / tail -40 text of two files never interleave.
#
# THE FILE LIST IS STILL DERIVED FROM THE TREE (see checks.yml). The two typed
# lists below only SORT or SET ASIDE files that exist; a new test_*.py that is
# in neither list is run, in the parallel batch. A name that no longer exists
# in a list is harmless. Nothing can make a file silently not run.
#
# SERIAL GROUP: files whose assertions depend on the wall clock, so CPU load
# from siblings could flip them. Name and reason, one each:
# test_loop.py drives the real run_control() against a real wall-clock
# floor (~93 s on purpose); worst inter-cycle gap 3.0 s
# against a 4.5 s bound (TEST-30, CI-06).
# test_freeze.py part 3 runs the real sim/hwsim.py over a transport with a
# 16 s window, a 10 s hold and an alarm measured at 11.0 s:
# one poll of margin, and "failed 1 run in 3" on the rig
# transport (TEST-30, CI-06).
# test_safety87.py builds a controller (CH65) whose meter reading is stamped
# on the REAL clock, and section 8 asks its status page,
# which says "Not ready - Meter: no reading for N seconds,
# limit 30 seconds" once N passes 30. Measured by CI-15: N is
# about half the file's run time (N = 45 s in a 94 s run, 48 s
# in a 103 s run). The file takes ~25 s idle and took 45 s in
# CI's first parallel run, so N was ~20 s of the 30. It FAILED
# alone under --cpus 0.15 (94 s) and in a loaded parallel run
# (103 s). No test file is changed here, so it runs alone.
# Add a file here only with a measured reason, and say it in this list.
SERIAL="test_loop.py test_freeze.py test_safety87.py"
#
# LONGEST FIRST (SAFETY-125's CI run, job 2580; then a local parallel run, seconds:
# modes_p4 758, safety126 634 (SAFETY-126, local, one core), safety125 508 on hotfix-0.19.2 only, modes_p3 364, safety119 328,
# price_run_fixes 184, safety90 160, safety103 134, safety94 114, peakguard 113,
# opt20 102, safety97 101, safety81 100). With JOBS workers pulling from one
# queue, starting the longest files first keeps the tail short: the batch takes
# about as long as the longest file. A sort order, not a selection.
HEAVY="test_modes_p4.py test_safety126.py test_safety125.py test_modes_p3.py test_safety119.py test_price_run_fixes.py test_safety90.py test_safety103.py test_safety94.py test_peakguard.py test_opt20.py test_safety97.py test_safety81.py"
#
JOBS=${CI_JOBS:-6}
cd "${TESTS_DIR:-/w/goodwe-addon/goodwe_controller}"
# one FILE: run it, keep its output in its own file, print its block whole.
if [ "${1:-}" = "--one" ]; then
f=$2
o="$RUN_DIR/$f.out"
start=$(date +%s)
if python3 "$f" > "$o" 2>&1; then rc=0; else rc=1; fi
secs=$(( $(date +%s) - start ))
{
if [ $rc = 0 ]; then
echo "PASS $f (${secs}s)"
grep "^CI: " "$o" || true
else
echo "FAIL $f (${secs}s)"
tail -40 "$o"
fi
} > "$RUN_DIR/$f.blk"
echo $rc > "$RUN_DIR/$f.rc"
flock "$RUN_DIR/print.lock" cat "$RUN_DIR/$f.blk"
exit 0
fi
set -- test_*.py
if [ "$1" = "test_*.py" ]; then
echo "NO test_*.py FOUND - the mount is empty or the layout moved. NOTHING has been checked."
exit 1
fi
echo "$# test file(s) to run: $JOBS at a time, then the serial group"
RUN_DIR=$(mktemp -d /tmp/ci-tests.XXXXXX)
export RUN_DIR
: > "$RUN_DIR/par"
: > "$RUN_DIR/ser"
for f in $HEAVY; do
case " $SERIAL " in *" $f "*) continue ;; esac
if [ -f "$f" ]; then echo "$f" >> "$RUN_DIR/par"; fi
done
for f in "$@"; do
case " $SERIAL $HEAVY " in *" $f "*) continue ;; esac
echo "$f" >> "$RUN_DIR/par"
done
for f in $SERIAL; do
if [ -f "$f" ]; then echo "$f" >> "$RUN_DIR/ser"; fi
done
echo "parallel: $(wc -l < "$RUN_DIR/par") file(s); serial: $(wc -l < "$RUN_DIR/ser") file(s)"
t0=$(date +%s)
rc=0
# `exit 0` in the worker: a failing FILE is recorded in its .rc, so a non-zero
# from xargs itself means the machinery broke, which is a failure too.
xargs -n 1 -P "$JOBS" sh -eu "$0" --one < "$RUN_DIR/par" || rc=1
echo "parallel batch done in $(( $(date +%s) - t0 ))s; serial group next"
while read -r f; do
sh -eu "$0" --one "$f" || rc=1
done < "$RUN_DIR/ser"
# Every file that was meant to run has a verdict, and every verdict is PASS. A
# worker that died before writing its .rc leaves no file, which counts as a fail.
ran=0
bad=0
for f in "$@"; do
ran=$((ran + 1))
if [ ! -f "$RUN_DIR/$f.rc" ]; then
echo "FAIL $f (no result: the run never reported)"
bad=$((bad + 1))
elif [ "$(cat "$RUN_DIR/$f.rc")" != 0 ]; then
bad=$((bad + 1))
fi
done
echo "$ran file(s), $bad failed, $(( $(date +%s) - t0 ))s wall"
if [ "$bad" = 0 ] && [ "$rc" = 0 ]; then exit 0; fi
exit 1