CI-12: CI grades test_freeze.py against the real hwsim at a pinned admin/goodwe
- .gitea/infra-pin: the one place the admin/goodwe commit is pinned. - checks.yml: checks out admin/goodwe at the pin as the sibling test_freeze.py looks for; any failure fails the job. Tests run with GOODWE_REQUIRE_HWSIM=1, and `CI: ` lines are echoed on PASS so the log shows which transport ran. - test_freeze.py: GOODWE_REQUIRE_HWSIM refuses the stub; a local stub run prints a STUB USED line. - infra-pin.yml: a separate, non-required check that goes red naming both SHAs when admin/goodwe master has moved past the pin. - README: when and how to bump the pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
# The admin/goodwe commit CI tests against. Bump: set to the full SHA of admin/goodwe master HEAD (README.md, "CI and the infra pin").
|
||||||
|
a783602ea131d49bc0c904333c1ccb101aac02d0
|
||||||
+25
-11
@@ -488,6 +488,26 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
echo "OK: the \`on:\` block of the merged checks.yml carries no event filter."
|
echo "OK: the \`on:\` block of the merged checks.yml carries no event filter."
|
||||||
|
|
||||||
|
# CI-12: the infra repo at the ONE pinned commit in .gitea/infra-pin (read
|
||||||
|
# from the merged tree), as the sibling test_freeze.py looks for. Any
|
||||||
|
# failure here fails the job; the tests step below refuses the stub.
|
||||||
|
- name: check out admin/goodwe at the pin in .gitea/infra-pin
|
||||||
|
env:
|
||||||
|
URL: ${{ github.server_url }}/admin/goodwe.git
|
||||||
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
PIN=$(grep -v '^#' "$WS/goodwe-addon/.gitea/infra-pin" | tr -d '[:space:]')
|
||||||
|
echo "$PIN" | grep -Eq '^[0-9a-f]{40}$' || { echo "FAIL: .gitea/infra-pin holds '$PIN', not a full 40-hex SHA."; exit 1; }
|
||||||
|
rm -rf "$WS/goodwe" && mkdir -p "$WS/goodwe" && cd "$WS/goodwe"
|
||||||
|
git init -q .
|
||||||
|
git remote add origin "$(echo "$URL" | sed "s#://#://x-access-token:${TOKEN}@#")"
|
||||||
|
git fetch -q origin "$PIN" || { echo "FAIL: could not fetch admin/goodwe at $PIN."; exit 1; }
|
||||||
|
git -c advice.detachedHead=false checkout -qf FETCH_HEAD
|
||||||
|
test "$(git rev-parse HEAD)" = "$PIN"
|
||||||
|
test -f sim/hwsim.py || { echo "FAIL: admin/goodwe@$PIN has no sim/hwsim.py."; exit 1; }
|
||||||
|
echo "infra: admin/goodwe @ $(git log --oneline -1)"
|
||||||
|
|
||||||
- name: build the test image (shipped deps, from apk - see .gitea/ci/Dockerfile)
|
- name: build the test image (shipped deps, from apk - see .gitea/ci/Dockerfile)
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
@@ -508,16 +528,9 @@ jobs:
|
|||||||
# shorter timeout - a 60 s limit was tried during CI-01 and reported
|
# shorter timeout - a 60 s limit was tried during CI-01 and reported
|
||||||
# exactly the false hang the docstring predicts.
|
# exactly the false hang the docstring predicts.
|
||||||
#
|
#
|
||||||
# ⚠️ WHAT THIS RUN DOES NOT COVER, stated because a pass here would
|
# test_freeze.py part 3 runs against the real `sim/hwsim.py` from the
|
||||||
# otherwise over-claim. test_freeze.py's third section wants the infra
|
# checkout above; GOODWE_REQUIRE_HWSIM=1 makes it fail rather than use its
|
||||||
# repo's `sim/hwsim.py` to drive a real HTTP meter through the real
|
# stub (CI-12). Lines a test prints with a `CI: ` prefix are echoed on PASS.
|
||||||
# transport. admin/goodwe is PRIVATE (checked via the API) and this
|
|
||||||
# workflow's GITHUB_TOKEN is minted for THIS repository only, so hwsim is
|
|
||||||
# absent here and `find_hwsim()` returns None. The rest of the file runs
|
|
||||||
# and passes. It does so SILENTLY - the file prints nothing to say the
|
|
||||||
# hwsim section was skipped - so "test_freeze.py: PASS" below is a weaker
|
|
||||||
# claim than it looks. That belongs to whoever owns test_freeze.py next;
|
|
||||||
# naming it here is the least this gate can do about it.
|
|
||||||
# ⚠️ PYTHONDONTWRITEBYTECODE=1 IS BELT AND BRACES, NOT A FIX FOR A LIVE
|
# ⚠️ PYTHONDONTWRITEBYTECODE=1 IS BELT AND BRACES, NOT A FIX FOR A LIVE
|
||||||
# HOLE. TEST-15 measured this runner's actual exposure and found none: the
|
# HOLE. TEST-15 measured this runner's actual exposure and found none: the
|
||||||
# checkout below is bind mounted `:ro`, so the interpreter physically
|
# checkout below is bind mounted `:ro`, so the interpreter physically
|
||||||
@@ -533,7 +546,7 @@ jobs:
|
|||||||
- name: the add-on's own tests
|
- name: the add-on's own tests
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
docker run --rm -e PYTHONDONTWRITEBYTECODE=1 -v "$WS:/w:ro" goodwe-addon-checks:1 sh -eu -c '
|
docker run --rm -e PYTHONDONTWRITEBYTECODE=1 -e GOODWE_REQUIRE_HWSIM=1 -v "$WS:/w:ro" goodwe-addon-checks:1 sh -eu -c '
|
||||||
cd /w/goodwe-addon/goodwe_controller
|
cd /w/goodwe-addon/goodwe_controller
|
||||||
set -- test_*.py
|
set -- test_*.py
|
||||||
if [ "$1" = "test_*.py" ]; then
|
if [ "$1" = "test_*.py" ]; then
|
||||||
@@ -546,6 +559,7 @@ jobs:
|
|||||||
start=$(date +%s)
|
start=$(date +%s)
|
||||||
if python3 "$f" > /tmp/out 2>&1; then
|
if python3 "$f" > /tmp/out 2>&1; then
|
||||||
echo "PASS $f ($(( $(date +%s) - start ))s)"
|
echo "PASS $f ($(( $(date +%s) - start ))s)"
|
||||||
|
grep "^CI: " /tmp/out || true
|
||||||
else
|
else
|
||||||
echo "FAIL $f ($(( $(date +%s) - start ))s)"
|
echo "FAIL $f ($(( $(date +%s) - start ))s)"
|
||||||
tail -40 /tmp/out
|
tail -40 /tmp/out
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# CI-12: goes red when admin/goodwe master has moved past .gitea/infra-pin, so
|
||||||
|
# the pin cannot go stale silently. Deliberately NOT a required check: it
|
||||||
|
# reports; `checks / unit` stays the only gate. How to bump: README.md,
|
||||||
|
# "CI and the infra pin".
|
||||||
|
name: infra-pin
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches: [release/1.0]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
fresh:
|
||||||
|
runs-on: [self-hosted, goodwe-rig]
|
||||||
|
timeout-minutes: 30 # seconds of work; the rest is queueing for the shared slot
|
||||||
|
steps:
|
||||||
|
- name: compare .gitea/infra-pin with admin/goodwe master
|
||||||
|
env:
|
||||||
|
SELF: ${{ github.server_url }}/${{ github.repository }}.git
|
||||||
|
INFRA: ${{ github.server_url }}/admin/goodwe.git
|
||||||
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
auth() { echo "$1" | sed "s#://#://x-access-token:${TOKEN}@#"; }
|
||||||
|
D=$(mktemp -d) && cd "$D"
|
||||||
|
git init -q . && git remote add origin "$(auth "$SELF")"
|
||||||
|
# The pin of the commit this event is about (the PR head, or the push).
|
||||||
|
git fetch -q --depth 1 origin "$GITHUB_SHA"
|
||||||
|
PIN=$(git show FETCH_HEAD:.gitea/infra-pin | grep -v '^#' | tr -d '[:space:]')
|
||||||
|
HEAD=$(git ls-remote "$(auth "$INFRA")" refs/heads/master | cut -f1)
|
||||||
|
cd / && rm -rf "$D"
|
||||||
|
test -n "$HEAD" || { echo "FAIL: could not read admin/goodwe master."; exit 1; }
|
||||||
|
if [ "$PIN" != "$HEAD" ]; then
|
||||||
|
echo "STALE: .gitea/infra-pin is $PIN but admin/goodwe master is $HEAD."
|
||||||
|
echo " Test against $HEAD and bump the pin (README.md, \"CI and the infra pin\")."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "OK: .gitea/infra-pin $PIN is admin/goodwe master HEAD."
|
||||||
@@ -105,6 +105,11 @@ has the mechanism, the measurement, and what the flags do not do. They are not u
|
|||||||
each assertion corresponds to a rule whose absence produced an observed failure
|
each assertion corresponds to a rule whose absence produced an observed failure
|
||||||
on real hardware, and the comments in `control.py` say which.
|
on real hardware, and the comments in `control.py` say which.
|
||||||
|
|
||||||
|
### CI and the infra pin
|
||||||
|
|
||||||
|
CI (`.gitea/workflows/checks.yml`) runs every `test_*.py`, with `admin/goodwe` checked out next to this repo at the commit in `.gitea/infra-pin`, so `test_freeze.py` grades against the real `sim/hwsim.py`. If that checkout fails, the job fails. The local stub is used only in local runs, and it prints `STUB USED` when it is.
|
||||||
|
**Bump the pin** whenever `infra-pin / fresh` is red, which happens once `admin/goodwe` master has moved past the pin. Set `.gitea/infra-pin` to the new master SHA (`git ls-remote <gitea>/admin/goodwe.git refs/heads/master`) in a PR, and let `checks / unit` grade the tests against it.
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
**Check the serial number, not the model name.** Characters 6-8 of the serial are
|
**Check the serial number, not the model name.** Characters 6-8 of the serial are
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ shipped `meter_freeze_s` itself:
|
|||||||
a check that calls the predicate directly would not be testing the thing
|
a check that calls the predicate directly would not be testing the thing
|
||||||
that ships. Uses the rig's own `sim/hwsim.py --fault freeze` when the infra
|
that ships. Uses the rig's own `sim/hwsim.py --fault freeze` when the infra
|
||||||
repo is next to this one (or `$GOODWE_SIM`), and an equivalent local stub
|
repo is next to this one (or `$GOODWE_SIM`), and an equivalent local stub
|
||||||
otherwise. Both are exercised; the run says which one it used.
|
otherwise. Both are exercised; the run says which one it used. CI sets
|
||||||
|
`GOODWE_REQUIRE_HWSIM=1`, which refuses the stub (CI-12).
|
||||||
|
|
||||||
4. THE WIRING: the real `Controller.cycle()`, asserted on the service call
|
4. THE WIRING: the real `Controller.cycle()`, asserted on the service call
|
||||||
that leaves the process;
|
that leaves the process;
|
||||||
@@ -600,6 +601,15 @@ hw = find_hwsim()
|
|||||||
# that cannot fail, from the other end.
|
# that cannot fail, from the other end.
|
||||||
PART3_TRANSPORT = f"the rig hwsim at {hw}" if hw else "the local stub"
|
PART3_TRANSPORT = f"the rig hwsim at {hw}" if hw else "the local stub"
|
||||||
print(f"frozen meter over HTTP - using {PART3_TRANSPORT}")
|
print(f"frozen meter over HTTP - using {PART3_TRANSPORT}")
|
||||||
|
# CI-12: CI sets GOODWE_REQUIRE_HWSIM=1, so a missing infra checkout fails here
|
||||||
|
# instead of passing on the stub. Local runs may still use the stub, and say so.
|
||||||
|
if not hw:
|
||||||
|
if os.environ.get("GOODWE_REQUIRE_HWSIM"):
|
||||||
|
print("FAIL: GOODWE_REQUIRE_HWSIM is set but sim/hwsim.py was not found "
|
||||||
|
"(sibling ../goodwe or $GOODWE_SIM) - refusing to fall back to the stub")
|
||||||
|
sys.exit(1)
|
||||||
|
print("STUB USED: part 3 ran against the local stub, NOT the infra repo's "
|
||||||
|
"sim/hwsim.py - not a CI-grade result")
|
||||||
if hw:
|
if hw:
|
||||||
def argv_for(mode):
|
def argv_for(mode):
|
||||||
return [HWSIM_DRIVER, "8791" if mode == "freeze" else "8792", mode,
|
return [HWSIM_DRIVER, "8791" if mode == "freeze" else "8792", mode,
|
||||||
@@ -1263,7 +1273,8 @@ check("a Controller built with the key absent lands on the SHIPPED hold, so "
|
|||||||
_ctl_c.freeze_wd.cfg.hold_s == SHIPPED_HOLD_S)
|
_ctl_c.freeze_wd.cfg.hold_s == SHIPPED_HOLD_S)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print(f"part 3 (the live HTTP transport) ran against: {PART3_TRANSPORT}")
|
# `CI: ` lines are echoed into the CI log even on a pass (checks.yml).
|
||||||
|
print(f"CI: test_freeze part 3 (the live HTTP transport) ran against: {PART3_TRANSPORT}")
|
||||||
if fails:
|
if fails:
|
||||||
print(f"{len(fails)} FAILED: {', '.join(fails)}")
|
print(f"{len(fails)} FAILED: {', '.join(fails)}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user