diet-guard/scripts/ci_mirror.sh
Krzysztof kuhy Rudnicki 665b0bf935
Some checks are pending
pre-commit / pre-commit (push) Waiting to run
Tests / test (3.10) (push) Waiting to run
Tests / test (3.11) (push) Waiting to run
Tests / test (3.12) (push) Waiting to run
release-apk / release (APK) (push) Waiting to run
Stop two keyboard tests hanging, and make the CI mirror mirror CI
Two failures, both pre-existing and both invisible as failures — each
reported only as "did not complete" after stalling the suite.

app/test/keyboard_operability_test.dart awaited writeLog() straight from
the test body. LogStorageService is backed by a FileDocumentStore there,
so that awaits real dart:io, and the fake-async zone never completes it:
the test hangs rather than fails. Seeding now happens inside runAsync
alongside the pumping, which is the only zone where that write can
resolve. The screen's own initState load needs the same real clock, and
pumpAndSettle() cannot be used to wait for it — inside runAsync it waits
on a frame scheduler this screen never reports as idle — so the helper
pumps a bounded number of times instead. 473 app tests now pass in 14s.

scripts/ci_mirror.sh ran bare pytest while python-tests.yml runs it under
xvfb-run. Without a display the gate's real-Tk tests collapse: the focus
ring has nothing to walk, so three TestRealFocusRing tests fail locally
and pass in CI. The mirror was reporting red on a green tree, which is
worse than not mirroring at all. It now uses the workflow's exact
xvfb-run invocation and fails loudly if xvfb-run is missing.

Neither change touches production code: the delete control and its
id-gate were already correct, and the Python suite already passed under a
display (609 passed, 100% coverage).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dzgcZxmpS3VtmLci2sbxj
2026-08-04 21:50:15 +02:00

145 lines
5.7 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# ci_mirror.sh — run the exact CI gates locally before a push can leave.
#
# Why: the installed pre-commit hook only checks *staged* files, and the local
# dev environment already has every dependency installed. CI does neither — it
# runs `pre-commit run --all-files` and installs a fresh environment from
# requirements.txt. That gap is why green-locally repos went red in CI
# (missing runtime deps, --all-files lint debt, order-dependent test flakes).
#
# This script reproduces both CI workflows on the developer machine:
# 1. a venv built ONLY from requirements.txt (mirrors the Tests workflow's
# `pip install -r requirements.txt`), rebuilt only when requirements.txt
# changes (hash-gated so day-to-day pushes stay fast);
# 2. `pre-commit run --all-files` (mirrors the pre-commit workflow);
# 3. `python -m pytest` inside that clean venv (mirrors Tests);
# 4. `flutter analyze` + `flutter test` for the companion app in app/.
#
# The app is gated here rather than only by pre-commit because pre-commit has
# no Dart hooks at all: without this, the app's tests were never run by
# anything automatic, and a red app/ could be pushed with a green gate.
#
# Wired as the pre-push hook, so a red result blocks the push before CI ever
# sees it. Escape hatch for genuine emergencies: `git push --no-verify`.
# ============================================================================
set -euo pipefail
# Requirements file may be overridden (e.g. testsAndMisc uses meta/…).
REQUIREMENTS_FILE="${REQUIREMENTS_FILE:-requirements.txt}"
readonly REQUIREMENTS_FILE
ROOT="$(git rev-parse --show-toplevel)"
readonly ROOT
cd "$ROOT"
readonly APP_DIR="$ROOT/app"
readonly VENV_DIR="$ROOT/.ci-mirror-venv"
readonly HASH_FILE="$VENV_DIR/.requirements.sha256"
readonly REQ_PATH="$ROOT/$REQUIREMENTS_FILE"
log() { printf 'ci-mirror: %s\n' "$1" >&2; }
fail() {
log "FAILED — $1"
log "CI would be red. Fix the above, or 'git push --no-verify' to override."
exit 1
}
require_file() {
if [[ ! -f "$REQ_PATH" ]]; then
fail "requirements file not found: $REQ_PATH"
fi
}
# Rebuild the venv only when requirements.txt changed since the last build.
ensure_venv() {
local current stored
current="$(sha256sum "$REQ_PATH" | cut -d' ' -f1)"
stored=""
if [[ -f "$HASH_FILE" ]]; then
stored="$(cat "$HASH_FILE")"
fi
if [[ -x "$VENV_DIR/bin/python" && "$current" == "$stored" ]]; then
log "venv up to date (requirements.txt unchanged)"
return
fi
log "requirements.txt changed — rebuilding clean venv (mirrors CI install)"
rm -rf "$VENV_DIR"
python3 -m venv "$VENV_DIR"
"$VENV_DIR/bin/python" -m pip install --quiet --upgrade pip \
|| fail "pip self-upgrade in the clean venv"
"$VENV_DIR/bin/python" -m pip install --quiet -r "$REQ_PATH" \
|| fail "pip install -r $REQUIREMENTS_FILE (a dep may be undeclared)"
printf '%s' "$current" > "$HASH_FILE"
log "clean venv ready"
}
run_precommit_all_files() {
log "pre-commit run --all-files (mirrors the pre-commit workflow)"
pre-commit run --all-files || fail "pre-commit --all-files"
}
run_pytest_clean_venv() {
log "pytest in the clean venv (mirrors the Tests workflow)"
# Under xvfb-run, exactly as python-tests.yml does. The gate's real-Tk
# tests walk a focus ring via tk_focusNext, which consults mapped-ness
# and real focus: on a developer's live display the window never takes
# focus and the ring collapses to one widget, so TestRealFocusRing fails
# here while passing in CI. Mirroring the workflow's display is the whole
# point of this script -- without it the mirror reports red on a green
# tree, which is worse than not mirroring at all.
if ! command -v xvfb-run >/dev/null 2>&1; then
fail "xvfb-run is not on PATH (install xorg-server-xvfb); the real-Tk
gate tests need the same virtual display the Tests workflow gives them"
fi
xvfb-run -a -s "-screen 0 1600x1200x24" \
"$VENV_DIR/bin/python" -m pytest "$@" \
|| fail "pytest (clean requirements.txt venv)"
}
# Run flutter with the calling git hook's environment stripped.
#
# `git push` exports GIT_DIR (and friends) to its hooks, and the flutter tool
# shells out to git to identify its own SDK. With GIT_DIR set it reads THIS
# repository instead of /opt/flutter: `flutter --version` then reports our
# HEAD as the framework revision and our URL as the repository, pub resolves
# the SDK as `0.0.0-unknown`, every version constraint fails, and the wrong
# answer is written into flutter's own version cache for the next caller.
flutter_clean_env() {
env -u GIT_DIR -u GIT_WORK_TREE -u GIT_INDEX_FILE -u GIT_PREFIX \
-u GIT_COMMON_DIR -u GIT_OBJECT_DIRECTORY flutter "$@"
}
# Gate the Flutter companion app the same way: analyze (pre-commit has no Dart
# hooks) then the full test suite. Deliberately fails rather than skips when
# flutter is missing -- a gate that quietly passes on a machine without the
# toolchain is the same as no gate.
run_flutter_gates() {
if [[ ! -f "$APP_DIR/pubspec.yaml" ]]; then
log "no app/pubspec.yaml — skipping the Flutter gates"
return
fi
if ! command -v flutter >/dev/null 2>&1; then
fail "flutter is not on PATH but app/ exists (cannot verify the app)"
fi
log "flutter analyze (app/)"
(cd "$APP_DIR" && flutter_clean_env analyze) || fail "flutter analyze"
log "flutter test (app/)"
(cd "$APP_DIR" && flutter_clean_env test) || fail "flutter test"
}
main() {
require_file
ensure_venv
run_precommit_all_files
run_pytest_clean_venv "$@"
run_flutter_gates
log "all CI gates passed locally — safe to push"
}
main "$@"