Files
shturman/tests/e2e/run.sh
T
kk0t9 93382d2de6 fix(v0.3): E2E power-safe-блок по реальным ошибкам Lima
- machineid bind (/data/state/machine-id→/etc/machine-id) держит /data busy →
  снимаем bind перед umount; remount через systemctl start (не mount /data — нет fstab).
- restart power/settings после install (повторный just run: иначе крутится старый
  бинарь, start=no-op) + restart power в начале power-safe-блока (чистый FSM Running).
- §3 oneshot-чек: firstboot/machineid валидны в active|inactive (отработал ИЛИ корректно
  пропущен Condition'ом на повторном boot); реальный сбой = failed.
- stage2.target тянет savetime.timer (WantedBy без enable не срабатывал).
Блок power-safe зелёный: N=3 цикла /data цел, abort→ShutdownAborted, power-cut fsck-clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Alexander <akotenev2003@gmail.com>
2026-06-24 23:33:36 +03:00

306 lines
21 KiB
Bash

#!/usr/bin/env bash
# Сквозной E2E Штурмана в Lima-VM (приёмка v0.1/v0.6 + шагающий скелет, спека §9.3/§9.4).
# Запуск: just e2e (двухфазно через reboot) или just run (однофазно, без reboot).
#
# Фазы (env E2E_PHASE):
# pre — сборка → install бинарей/юнитов → старт shturman.target → проверки §9.3 (1–8)
# → выставить персист-пробу (Settings.Set + запомнить machine-id); [по умолчанию]
# post — после reboot: персист настройки сохранился + machine-id стабилен (every-boot bind, §9.3.4).
#
# Системная шина устройства (§5.1). dev-mocks включён дефолт-фичей сборки (fake-ACC).
set -uo pipefail
REPO=/shturman
cd "$REPO" || { echo "нет $REPO"; exit 1; }
PHASE="${E2E_PHASE:-pre}"
IDLE_SECS="${E2E_IDLE_SECS:-20}" # окно простоя для eMMC-прокси (§7.5)
EMMC_MAX_SECTORS="${E2E_EMMC_MAX_SECTORS:-4096}" # порог T (🟡 калибруется; 4096 сект ≈ 2 МБ/окно)
FRAME=/run/shturman/frame.png
PROBE_KEY=ui.theme
PROBE_VAL=night
MID_BEFORE=/data/state/e2e-mid-before # снимок machine-id до reboot (персист в /data)
export PATH="$HOME/.cargo/bin:$PATH"
export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$HOME/.cache/shturman/target}"
pass() { echo " ✓ $*"; }
fail() { echo "E2E FAIL: $*" >&2; exit 1; }
info() { echo; echo "== $* =="; }
# Имена на шине (зеркало crates/shturman-ipc/src/names.rs).
P_NAME=ru.shturman.Power; P_PATH=/ru/shturman/Power; P_IFACE=ru.shturman.Power1
P_MOCK=ru.shturman.dev.PowerMock1
S_NAME=ru.shturman.Settings; S_PATH=/ru/shturman/Settings; S_IFACE=ru.shturman.Settings1
settings_get() { busctl --system call "$S_NAME" "$S_PATH" "$S_IFACE" Get s "$1" 2>/dev/null; }
# =========================== POST-reboot фаза ===========================
if [ "$PHASE" = post ]; then
info "POST-reboot: персист настроек + machine-id стабилен (§9.3.4)"
# дождаться, пока сервисы поднимутся после автозагрузки target
for _ in $(seq 1 30); do systemctl is-active --quiet shturman-settings && break; sleep 1; done
findmnt /data >/dev/null || fail "/data не смонтирован после reboot"
pass "/data смонтирован после reboot"
# настройка пережила reboot
got=$(settings_get "$PROBE_KEY")
echo "$got" | grep -q "\"$PROBE_VAL\"" || fail "Settings.$PROBE_KEY != $PROBE_VAL после reboot (got: $got)"
pass "Settings.$PROBE_KEY = $PROBE_VAL пережил reboot"
# machine-id стабилен (every-boot bind из /data/state/machine-id)
sudo test -f "$MID_BEFORE" || fail "нет снимка $MID_BEFORE (фаза pre не отработала?)"
before=$(sudo cat "$MID_BEFORE"); now=$(cat /etc/machine-id)
[ -n "$now" ] || fail "/etc/machine-id пуст"
[ "$before" = "$now" ] || fail "machine-id изменился: было $before, стало $now"
src=$(sudo cat /data/state/machine-id)
[ "$now" = "$src" ] || fail "/etc/machine-id($now) != /data/state/machine-id($src) — bind не сработал"
pass "machine-id стабилен после reboot ($now), привязан из /data"
# journald volatile теперь естественно (drop-in присутствовал на boot)
test -d /run/log/journal && ! test -d /var/log/journal \
&& pass "journald volatile (/run/log/journal)" || echo " WARN: journald не строго volatile"
echo; echo "E2E POST OK ✅"
exit 0
fi
# ============================ PRE фаза ============================
info "сборка (release, VM-локальный target=$CARGO_TARGET_DIR)"
cargo build --release --workspace || fail "сборка"
for b in firstboot settings power shell splash; do
sudo install -m755 "$CARGO_TARGET_DIR/release/shturman-$b" /usr/local/bin/ || fail "install shturman-$b"
done
pass "бинари установлены в /usr/local/bin"
info "раскладка systemd-юнитов + dbus policy (из репо — подхватить правки)"
sudo install -m644 systemd/shturman.target systemd/data.mount \
systemd/shturman-stage0.target systemd/shturman-stage1.target systemd/shturman-stage2.target \
/etc/systemd/system/
sudo install -m644 systemd/shturman-firstboot.service systemd/shturman-machineid.service \
systemd/shturman-power.service systemd/shturman-settings.service \
systemd/shturman-shell.service systemd/shturman-splash.service \
systemd/shturman-stage2-warmup.service systemd/shturman-savetime.service \
/etc/systemd/system/
sudo install -d /etc/dbus-1/system.d
sudo install -m644 systemd/dbus/ru.shturman.conf /etc/dbus-1/system.d/
sudo install -d /etc/systemd/journald.conf.d /etc/systemd/oomd.conf.d
sudo install -m644 systemd/journald-shturman.conf /etc/systemd/journald.conf.d/shturman.conf
sudo install -m644 systemd/oomd-shturman.conf /etc/systemd/oomd.conf.d/shturman.conf
sudo install -m644 systemd/zram-generator.conf /etc/systemd/zram-generator.conf
sudo install -d /etc/tmpfiles.d
sudo install -m644 systemd/tmpfiles-shturman.conf /etc/tmpfiles.d/shturman.conf
sudo systemd-tmpfiles --create /etc/tmpfiles.d/shturman.conf || true
# watchdog (B05/A14) + save-time .timer (B07)
sudo install -d /etc/systemd/system.conf.d
sudo install -m644 systemd/watchdog-shturman.conf /etc/systemd/system.conf.d/shturman-watchdog.conf
sudo install -m644 systemd/shturman-savetime.timer /etc/systemd/system/
sudo systemctl daemon-reload
# применить конфиги детерминированно (на свежем boot drop-in’ы появились после старта демонов)
sudo systemctl reload dbus 2>/dev/null || true
sudo systemctl restart systemd-journald 2>/dev/null || true
sudo rm -rf /var/log/journal 2>/dev/null || true # устаревший persistent-журнал (до drop-in); volatile его не пересоздаст
sudo modprobe zram 2>/dev/null || true # zram-модуль (linux-modules-extra); может отсутствовать в vz-ядре
sudo systemctl start "systemd-zram-setup@zram0.service" 2>/dev/null || true
sudo systemctl restart systemd-oomd 2>/dev/null || sudo systemctl start systemd-oomd 2>/dev/null || true # подхватить oomd.conf.d
pass "юниты/политики разложены"
info "старт shturman.target (зонтик → Stage 0/1/2)"
sudo systemctl start shturman.target || fail "shturman.target не стартовал"
# Перезапустить демоны, чтобы подхватить свежесобранные бинари (на повторном just run — иначе крутится
# старый бинарь, start=no-op; на чистом vm-reset сервисы и так стартуют с новым). shell НЕ трогаем:
# иначе frame.png стал бы новее stage2.ready и сломал бы ассерт «warmup после кадра» (порядок фаз).
sudo systemctl restart shturman-power.service shturman-settings.service
for _ in $(seq 1 15); do systemctl is-active --quiet shturman-shell && break; sleep 1; done
# ---- 1. /data до сервисов + реальные power-safe опции (§9.3.1) ----
info "1. /data смонтирован, реальные non-default опции + volatile-слой"
findmnt /data >/dev/null || fail "/data не смонтирован"
opts=$(findmnt -no OPTIONS /data)
echo "$opts" | grep -q errors=remount-ro || fail "нет errors=remount-ro (opts: $opts)"
pass "/data: $opts"
# volatile-слой (tmpfs) присутствует — кадр/журнал/транзиент пишутся сюда, не на flash (A11).
# Полный RO-rootfs + overlay(upper на tmpfs) — на HW/v4 (A/B boot-select нет в VM, §7.1); тут — дисциплина + tmpfs.
findmnt -t tmpfs /run >/dev/null || fail "/run не tmpfs (нет volatile-слоя)"
pass "volatile-слой: /run = tmpfs"
# ---- 2. first-boot маркер + идемпотентность (§9.3.2) ----
info "2. first-boot маркер + machine-id"
sudo test -f /data/.shturman-provisioned || fail "нет маркера .shturman-provisioned"
sudo test -f /data/state/machine-id || fail "нет /data/state/machine-id"
sudo systemctl start shturman-firstboot.service # повторно — Condition гейтит → no-op
pass "first-boot маркер на месте, повторный запуск no-op"
# ---- 3. per-unit critical set active (degraded не маскирует, §9.3.1) ----
info "3. per-unit critical set"
for u in shturman-power shturman-settings shturman-shell; do
systemctl is-active --quiet "$u" || fail "$u не active ($(systemctl is-active "$u" 2>&1))"
pass "$u: active"
done
for u in shturman-firstboot shturman-machineid; do
state=$(systemctl is-active "$u" 2>&1)
# oneshot валиден в active (отработал, RemainAfterExit) ИЛИ inactive (корректно пропущен Condition'ом
# на повторном boot: firstboot — marker есть; reflects clean+re-run). Реальный сбой = failed/activating.
case "$state" in
active | inactive) pass "$u: $state (oneshot)" ;;
*) fail "$u не active/inactive (сбой): $state" ;;
esac
done
# ---- 4. имена на системной шине (own) + сервис отвечает (§9.3.3) ----
info "4. имена на шине + отклик"
busctl --system list | grep -q "$P_NAME" || fail "нет $P_NAME на шине"
busctl --system list | grep -q "$S_NAME" || fail "нет $S_NAME на шине"
busctl --system call "$P_NAME" "$P_PATH" "$P_IFACE" GetPowerState | grep -q running \
|| fail "Power.GetPowerState != running"
pass "$P_NAME / $S_NAME владеют именами; GetPowerState=running"
# ---- 5. fake-ACC: SetAcc -> AccChanged (§9.3.5) ----
info "5. fake-ACC SetAcc -> AccChanged"
mon=$(mktemp)
# sudo нужен busctl для eavesdrop системной шины; редирект в $mon — намеренно user-owned mktemp (SC2024 ок).
# shellcheck disable=SC2024
sudo busctl --system monitor "$P_NAME" >"$mon" 2>&1 &
MON=$!
sleep 1
busctl --system call "$P_NAME" "$P_PATH" "$P_MOCK" SetAcc b false || { sudo kill "$MON" 2>/dev/null; fail "вызов SetAcc"; }
sleep 1
sudo kill "$MON" 2>/dev/null; wait "$MON" 2>/dev/null
grep -q AccChanged "$mon" || { echo "--- monitor ---"; cat "$mon"; fail "AccChanged не наблюдаем"; }
rm -f "$mon"
busctl --system call "$P_NAME" "$P_PATH" "$P_MOCK" SetAcc b true >/dev/null 2>&1 || true # вернуть acc=on
pass "AccChanged наблюдаем после SetAcc"
# ---- 7. первый Slint-кадр: PNG не пустой (§9.3.6) ----
info "7. первый Slint-кадр (software-render PNG)"
sudo test -f "$FRAME" || fail "нет кадра $FRAME (shell.service не отрендерил?)"
sz=$(sudo stat -c%s "$FRAME"); [ "$sz" -gt 10000 ] || fail "кадр подозрительно мал ($sz Б)"
sudo head -c8 "$FRAME" | od -An -tx1 | tr -d ' \n' | grep -qi "89504e47" || fail "$FRAME не PNG"
pass "кадр $FRAME: $sz Б, валидный PNG"
# ---- Stage 0/1/2 разделены (v0.2 boot-конвейер) ----
info "Stage 0/1/2: фазы разделены + splash до кадра + warmup после"
for t in shturman-stage0 shturman-stage1 shturman-stage2; do
systemctl is-active --quiet "$t.target" || fail "$t.target не достигнут ($(systemctl is-active "$t.target" 2>&1))"
pass "$t.target reached"
done
sudo test -f /run/shturman/splash.png || fail "нет splash.png (Stage 0)"
sudo head -c8 /run/shturman/splash.png | od -An -tx1 | tr -d ' \n' | grep -qi 89504e47 || fail "splash.png не PNG"
fr=$(sudo stat -c %Y "$FRAME"); sp=$(sudo stat -c %Y /run/shturman/splash.png)
[ "$sp" -le "$fr" ] || fail "splash.png ($sp) позже frame.png ($fr) — Stage 0 не раньше Stage 1"
sudo test -f /run/shturman/stage2.ready || fail "нет stage2.ready (Stage 2 warmup не отработал)"
w2=$(sudo stat -c %Y /run/shturman/stage2.ready)
[ "$w2" -ge "$fr" ] || fail "stage2.ready ($w2) раньше кадра ($fr) — Stage 2 не деферред"
pass "порядок фаз: splash($sp) ≤ frame($fr) ≤ stage2($w2)"
# boot-тайминг (функц., НЕ гейт; вердикт — RK3588, performance §2)
echo " $(systemd-analyze time 2>/dev/null | head -1 || echo 'systemd-analyze н/д')"
# ---- power-safe (v0.3): FSM ShutdownImminent + N циклов зажигания + abort + power-cut ----
info "power-safe: ShutdownImminent + N=3 цикла зажигания + abort + power-cut"
# Чистый FSM Running для циклов (свежий бинарь + сброс любого «залипшего» состояния от §5 fake-ACC).
sudo systemctl restart shturman-power.service
for _ in $(seq 1 10); do systemctl is-active --quiet shturman-power && break; sleep 1; done
sleep 1 # дать power re-acquire ru.shturman.Power на шине
P_CALL() { busctl --system call "$P_NAME" "$P_PATH" "$P_MOCK" "$@"; }
busctl --system call "$S_NAME" "$S_PATH" "$S_IFACE" Set sv ui.theme s night >/dev/null
echo 0 | sudo tee /data/state/power-cycles >/dev/null
observe_imminent() { # SetAcc(false) → ждём ShutdownImminent на шине
local mon; mon=$(mktemp)
# shellcheck disable=SC2024
sudo busctl --system monitor "$P_NAME" >"$mon" 2>&1 & local M=$!
sleep 0.7; P_CALL SetAcc b false >/dev/null; sleep 0.7
sudo kill "$M" 2>/dev/null; wait "$M" 2>/dev/null
grep -q ShutdownImminent "$mon" || { echo "--- mon ---"; cat "$mon"; rm -f "$mon"; return 1; }
rm -f "$mon"
}
for i in 1 2 3; do
observe_imminent || fail "цикл $i: ShutdownImminent не наблюдаем"
n=$(($(sudo cat /data/state/power-cycles) + 1))
sudo systemctl stop shturman-stage1.target # стоп сервисов (освобождает /data)
sudo umount /etc/machine-id 2>/dev/null || true # снять machineid-bind, иначе /data busy
sync; sudo umount /data || fail "цикл $i: umount /data (PONR)"
findmnt /data >/dev/null && fail "цикл $i: /data не размонтирован (PONR не достигнут)"
sudo systemctl start shturman.target # re-mount data.mount + сервисы (machineid re-bind)
for _ in $(seq 1 15); do systemctl is-active --quiet shturman-settings && break; sleep 1; done
findmnt /data >/dev/null || fail "цикл $i: /data не вернулся после remount"
echo "$n" | sudo tee /data/state/power-cycles >/dev/null
pass "цикл зажигания $i: stop→umount(PONR)→remount→restart, /data вернулся"
done
got=$(busctl --system call "$S_NAME" "$S_PATH" "$S_IFACE" Get s ui.theme 2>/dev/null)
echo "$got" | grep -q '"night"' || fail "ui.theme потерян после циклов"
[ "$(sudo cat /data/state/power-cycles)" = 3 ] || fail "счётчик циклов != 3"
pass "N=3 цикла: /data + счётчик целы (нет потери)"
# abort до PONR
mon=$(mktemp)
# shellcheck disable=SC2024
sudo busctl --system monitor "$P_NAME" >"$mon" 2>&1 & M=$!
sleep 0.7; P_CALL SetAcc b false >/dev/null; sleep 0.3; P_CALL SetAcc b true >/dev/null; sleep 0.7
sudo kill "$M" 2>/dev/null; wait "$M" 2>/dev/null
grep -q ShutdownAborted "$mon" || { cat "$mon"; rm -f "$mon"; fail "ShutdownAborted не наблюдаем"; }
rm -f "$mon"
findmnt /data >/dev/null || fail "/data не смонтирован после abort"
busctl --system call "$P_NAME" "$P_PATH" "$P_IFACE" GetPowerState | grep -q running || fail "не running после abort"
pass "abort до PONR: ShutdownAborted + /data RW + running"
# power-cut-сим: SIGKILL во время shutdown → /data консистентен
P_CALL SetAcc b false >/dev/null; sleep 0.3
sudo systemctl kill -s KILL shturman-power.service shturman-settings.service 2>/dev/null || true
sudo systemctl stop shturman-stage1.target 2>/dev/null || true
sudo umount /etc/machine-id 2>/dev/null || true
sudo umount /data 2>/dev/null || sudo umount -l /data 2>/dev/null || true
findmnt /data >/dev/null && fail "power-cut: /data не размонтирован (fsck был бы на смонтированном)"
sudo fsck.ext4 -n /var/lib/shturman/data.img >/dev/null 2>&1 || fail "fsck /data не clean после power-cut"
sudo systemctl start shturman.target # re-mount + restart
for _ in $(seq 1 15); do systemctl is-active --quiet shturman-settings && break; sleep 1; done
sudo grep -q night /data/settings/settings.json || fail "last durable value потерян после power-cut"
pass "power-cut-сим: /data консистентен (fsck clean, night present)"
# watchdog/save-time конфиг
test -f /etc/systemd/system.conf.d/shturman-watchdog.conf || fail "нет watchdog-конфига"
systemctl is-active --quiet shturman-savetime.timer && pass "savetime.timer активен" || echo " WARN: savetime.timer не активен"
pass "watchdog-конфиг на месте"
# ---- 8. base-бюджеты: journald / zram / fake-hwclock / eMMC-прокси (§9.3.7) ----
info "8. base-бюджеты (функц.)"
# journald volatile: активный журнал в /run/log/journal, persistent /var/log/journal отсутствует (A10)
test -d /run/log/journal || fail "journald не volatile (нет /run/log/journal)"
test -d /var/log/journal && fail "journald пишет в persistent /var/log/journal (нарушение A10)"
pass "journald volatile (/run/log/journal, без /var/log/journal)"
# ровно одно zram-устройство (A09); модуль zram может отсутствовать в vz-ядре — честная VM↔HW-граница
zn=$(zramctl --noheadings 2>/dev/null | wc -l | tr -d ' ')
if [ "$zn" = 1 ]; then pass "zram: ровно одно устройство ($(zramctl --noheadings --output NAME 2>/dev/null))"
elif [ "$zn" = 0 ]; then echo " WARN: zram-устройств нет (модуль zram отсутствует в vz-ядре — HW-only, §13)"
else fail "zram: ожидалось одно устройство, найдено $zn"; fi
# fake-hwclock пишет в /data, не в /etc (A11). Override — FILE из /etc/default/fake-hwclock
# (сервис в Lima masked: Lima сам синхронит время — на HW юнит размаскирован, EnvironmentFile тот же).
sudo sh -c '. /etc/default/fake-hwclock 2>/dev/null; FILE="${FILE:-/data/state/fake-hwclock.data}" fake-hwclock save' || true
sudo test -f /data/state/fake-hwclock.data || fail "fake-hwclock не записал в /data/state/fake-hwclock.data"
sudo test -f /etc/fake-hwclock.data && fail "fake-hwclock пишет в /etc (нарушение A11)"
pass "fake-hwclock → /data/state/fake-hwclock.data (не в /etc)"
# systemd-oomd: запущен + наша политика загружена (A09). PSI/cgroup2 нужны — в vz есть; иначе honest WARN.
if [ "$(systemctl is-active systemd-oomd 2>/dev/null)" = active ] && test -f /etc/systemd/oomd.conf.d/shturman.conf; then
pass "systemd-oomd active, политика oomd.conf.d/shturman.conf загружена"
else echo " WARN: systemd-oomd не active (нет PSI/пакета — проверь провижининг)"; fi
# eMMC-прокси: дельта записанных секторов loop-/data за окно простоя
src=$(findmnt -no SOURCE /data); dev=$(basename "$src")
if [ ! -e "/sys/block/$dev" ]; then dev=$(losetup -j /var/lib/shturman/data.img -O NAME --noheadings 2>/dev/null | tr -d ' ' | xargs -r basename); fi
read_w() { awk -v d="$dev" '$3==d {print $10}' /proc/diskstats; }
s0=$(read_w); echo " окно простоя ${IDLE_SECS}s (loop-dev=$dev)…"; sleep "$IDLE_SECS"; s1=$(read_w)
delta=$(( ${s1:-0} - ${s0:-0} ))
echo " eMMC-прокси: записано $delta секторов за ${IDLE_SECS}s (порог $EMMC_MAX_SECTORS, ~калибровка)"
[ "$delta" -le "$EMMC_MAX_SECTORS" ] || fail "eMMC: дельта $delta > порога $EMMC_MAX_SECTORS секторов"
pass "eMMC-прокси в пороге"
# ---- персист-проба для POST-фазы ----
info "персист-проба (для проверки после reboot)"
busctl --system call "$S_NAME" "$S_PATH" "$S_IFACE" Set sv "$PROBE_KEY" s "$PROBE_VAL" || fail "Settings.Set"
got=$(settings_get "$PROBE_KEY"); echo "$got" | grep -q "\"$PROBE_VAL\"" || fail "Set не применился (got: $got)"
sudo cp /etc/machine-id "$MID_BEFORE" # снимок до reboot
pass "Settings.$PROBE_KEY=$PROBE_VAL выставлен; machine-id снят в $MID_BEFORE"
echo; echo "E2E PRE OK ✅ (для полной приёмки — reboot + фаза post: just e2e)"