#!/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 /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 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 не стартовал" # Фазовый старт рендерит splash → frame → warmup по порядку (Before/After). Restart shell НЕ делаем: # иначе frame.png стал бы новее stage2.ready и сломал ассерт «warmup после кадра» (порядок фаз). 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) [ "$state" = active ] || fail "$u не active(exited): $state" pass "$u: $state (oneshot)" 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 н/д')" # ---- 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)"