diff --git a/.claude/skills/vm-install/SKILL.md b/.claude/skills/vm-install/SKILL.md index 4494133..c1d6692 100644 --- a/.claude/skills/vm-install/SKILL.md +++ b/.claude/skills/vm-install/SKILL.md @@ -63,7 +63,7 @@ and wait with an until-loop on the log, e.g. - Refuses an existing `disk.qcow2` without `--force` (asks YES), and a running VM (port 2222). -- Never edit `vminstall.sh` while it runs: bash reads it as it goes. +- Never edit a script in place while it runs (bash reads it as it goes; Python `open(p,"w")` and editors that rewrite do this). `sed -i` or writing a copy and `mv` swaps the file: the running one keeps its old copy. - Don't match your own wait loop with `pgrep -f`/`pkill -f` on a pattern that is part of that loop's command line. - Mirror 404s during the install are normal for an older ISO (pacman moves diff --git a/TODO.md b/TODO.md index 0bbffed..a873b59 100644 --- a/TODO.md +++ b/TODO.md @@ -39,7 +39,7 @@ R2.2 (first desktop login look), H-Real (real Steam Machine). - [ ] S1. Framework: `scripts/vmtest.sh` takes suites, resets the VM per block, tallies PASS/FAIL/SKIP, prints the skipped manual rows; move the loader matrix under suite `boot` - [x] S2. Suite `cli` (55 checks pass): G1-G3, F1-F7 (exit codes, `features.state`, boot-desktop unit) -- [ ] S3. Suite `menu`: R1.1-R1.6, R1.10 with scripted menu input (`vmrun.sh`) and `vmstate.sh` asserts +- [x] S3. Suite `menu` (36 checks pass): R1.1-R1.6, R1.10 with scripted menu input (`vmrun.sh`) and `vmstate.sh` asserts - [ ] S4. Suite `hw`: H1, H3 (`WIZARD_BIOS_DRY_RUN=1`), H4, H5, R1.7 (`vmcec.sh --no-sleep`) - [ ] S5. Suite `installer`: R2.1 (`vminstallsim.sh`) - [ ] S6. R1.8 toggle matrix and R1.9 reboot checks (from the skill), as suite `toggles` diff --git a/scripts/common.sh b/scripts/common.sh index ab89a6c..f715e64 100644 --- a/scripts/common.sh +++ b/scripts/common.sh @@ -19,3 +19,13 @@ if [[ -z "${VM_SSH_KEY:-}" && -f "$VM_DIR/ssh-key" ]]; then VM_SSH_KEY="$(cat "$ [[ -n "${VM_SSH_KEY:-}" ]] && VM_SSH_OPTS+=(-i "$VM_SSH_KEY" -o IdentitiesOnly=yes) vm_ssh() { ssh -p "$VM_PORT" "${VM_SSH_OPTS[@]}" "$VM_USER@$VM_HOST" "$@"; } vm_scp() { scp -q -P "$VM_PORT" "${VM_SSH_OPTS[@]}" "$@"; } + +# One Konsole for every test run: it follows $TEST_LOG, which the test scripts append to. +# Started only when there is a desktop, the run is headless and no such window exists. +TEST_LOG="${TEST_LOG:-$HOME/vms/test.log}" +view_start() { + [[ -n "${VM_HEADLESS:-}" && -z "${CI:-}" && -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]] && command -v konsole >/dev/null || return 0 + systemctl --user is-active --quiet vmtest-view 2>/dev/null && return 0 + mkdir -p "$(dirname "$TEST_LOG")"; touch "$TEST_LOG" + systemd-run --user -q --collect --unit=vmtest-view konsole --hold -e tail -n 60 -F "$TEST_LOG" >/dev/null 2>&1 || true +} diff --git a/scripts/vmbootloadertest.sh b/scripts/vmbootloadertest.sh old mode 100755 new mode 100644 index ff3e1d9..3f43b92 --- a/scripts/vmbootloadertest.sh +++ b/scripts/vmbootloadertest.sh @@ -42,14 +42,12 @@ stop_vm() { } start_vm() { stop_vm; cp "$repo/run.sh" "$VM_DIR/run.sh"; (cd "$VM_DIR" && REPO="${REPO:-$repo/../steamify-cachyos}" setsid nohup ./run.sh --fremont > vm.log 2>&1 &); } reboot_vm() { gssh 'sudo systemctl reboot' >/dev/null 2>&1; sleep 25; waitssh; } -# Headless on a desktop: a Konsole following the log (vmtest.sh opens one for all loaders; CI has no desktop). -if [[ -n "${VM_HEADLESS:-}" && -z "${CI:-}${VMTEST_NO_VIEW:-}" && -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]] && command -v konsole >/dev/null; then - systemd-run --user -q --collect --unit="vmtest-view-$loader-$$" konsole --hold -e tail -n +1 -F "$log" >/dev/null 2>&1 || true -fi +view_start # one shared Konsole (common.sh) # The guest checks print PASS/FAIL lines; the summary at the end counts them in the log. check() { cat; } { +printf '\n===== vmbootloadertest.sh %s (%s) =====\n' "$loader" "$(date +%T)" if $install; then step "install $loader VM in $VM_DIR" stop_vm @@ -112,7 +110,7 @@ b="$(gssh uname -r)" step "DONE" stop_vm -} 2>&1 | tee "$log" | sed 's/\x1b\[[0-9;]*m//g' +} 2>&1 | tee "$log" | tee -a "$TEST_LOG" | sed 's/\x1b\[[0-9;]*m//g' fails="$(grep -c '^FAIL' "$log")" echo; echo "== $loader: $(grep -c '^PASS' "$log") passed, $fails failed (log: $log)" grep '^FAIL' "$log" diff --git a/scripts/vmsuite.sh b/scripts/vmsuite.sh old mode 100755 new mode 100644 index 2798ac6..e1c124b --- a/scripts/vmsuite.sh +++ b/scripts/vmsuite.sh @@ -17,18 +17,16 @@ dir="$repo/share/vmtest/$suite" [[ -d "$dir" ]] || { echo "No suite $suite (folders in share/vmtest/)" >&2; exit 2; } . "$here/common.sh" log="$VM_DIR.$suite.log" -# Headless on a desktop: a Konsole following the log (CI has no desktop; vmtest.sh opens its own). -if [[ -n "${VM_HEADLESS:-}" && -z "${CI:-}${VMTEST_NO_VIEW:-}" && -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]] && command -v konsole >/dev/null; then - systemd-run --user -q --collect --unit="vmsuite-view-$suite-$$" konsole --hold -e tail -n +1 -F "$log" >/dev/null 2>&1 || true -fi +view_start # one shared Konsole (common.sh), headless on a desktop { +printf '\n===== vmsuite.sh %s (%s) =====\n' "$suite" "$(date +%T)" for block in "$dir"/*.sh; do [[ -z "${ONLY:-}" || "$(basename "$block")" == "$ONLY"* ]] || continue # ONLY=50: just that block printf '\n##### %s %s\n' "$(date +%T)" "$(basename "$block" .sh)" "$here/vmreset.sh" --fremont > /tmp/vmsuite-reset.out 2>&1 || { echo "FAIL vmreset.sh failed: $(tail -n 1 /tmp/vmsuite-reset.out)"; continue; } - cat "$repo/share/vmtest/prelude.sh" "$block" | vm_ssh -o ConnectTimeout=6 -o LogLevel=ERROR bash -s 2>&1 | sed 's/\x1b\[[0-9;]*m//g' + cat "$repo/share/vmtest/prelude.sh" "$block" | vm_ssh -o ConnectTimeout=6 -o LogLevel=ERROR bash -s 2>&1 | sed -u 's/\x1b\[[0-9;]*m//g' done -} 2>&1 | tee "$log" +} 2>&1 | tee "$log" | tee -a "$TEST_LOG" fails="$(grep -c '^FAIL' "$log")" echo; echo "== $suite: $(grep -c '^PASS' "$log") passed, $fails failed, $(grep -c '^SKIP' "$log") skipped (log: $log)" grep '^FAIL' "$log" diff --git a/scripts/vmtest.sh b/scripts/vmtest.sh old mode 100755 new mode 100644 index 25aebb3..19d6bfc --- a/scripts/vmtest.sh +++ b/scripts/vmtest.sh @@ -27,12 +27,7 @@ for l in $advertised; do [[ " $tested " == *" $l "* ]] || { echo "FAIL the ISO advertises $l, but there is no test for it (scripts/vmbootloadertest.sh)"; missing=$((missing + 1)); } done total=$missing -# No human is needed: headless. One Konsole for all the logs (the loader scripts open none). -export VMTEST_NO_VIEW=1 -if [[ -z "$window" && -z "${CI:-}" && -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ]] && command -v konsole >/dev/null; then - logs=(); for l in "${loaders[@]}"; do logs+=("$HOME/vms/bl-$l.test.log"); done - systemd-run --user -q --collect --unit="vmtest-view-$$" konsole --hold -e tail -n +1 -F "${logs[@]}" >/dev/null 2>&1 || true -fi +# No human is needed: headless; the scripts share one Konsole (common.sh, $TEST_LOG). for l in "${loaders[@]}"; do "$here/vmbootloadertest.sh" "$l" $install $window > "/tmp/vmtest-$l.out" 2>&1; rc=$? sed -n '/^== /,$p' "/tmp/vmtest-$l.out" diff --git a/share/vmtest/menu/10-r1-menu.sh b/share/vmtest/menu/10-r1-menu.sh new file mode 100644 index 0000000..31999be --- /dev/null +++ b/share/vmtest/menu/10-r1-menu.sh @@ -0,0 +1,42 @@ +# R1.1-R1.6, R1.10: the menu on an existing desktop, driven with scripted input (each row builds on the last). +menu() { printf "$1" | bash /mnt/steamify.sh 2>&1 | sed 's/\x1b\[[0-9;]*m//g'; } +# R1.1: first-run ticks: everything but Boot into desktop and BIOS +out=$(menu 'q\n') +for n in 1 3 4 5 6 7 8 9 10; do grep -qE "^ +$n +[a-z-]+ +\[x\]" <<< "$out" && pass "R1.1 row $n ticked" || fail "R1.1 row $n not ticked"; done +grep -qE "^ +11 .*\[ \]" <<< "$out" && pass "R1.1 Update BIOS not ticked" || fail "R1.1 Update BIOS ticked" +grep -q "Boot into: \[gamescope\]" <<< "$out" && pass "R1.1 boots into gamescope by default" || fail "R1.1 boot default is not gamescope" +# R1.2: everything OK, back in the menu with all on +out=$(menu '\ny\nm\nq\nn\n') +grep -q '^\[ERROR\]' <<< "$out" && fail "R1.2 errors: $(grep -m1 '^\[ERROR\]' <<< "$out")" || pass "R1.2 no errors" +grep -q "^\[OK\]" <<< "$out" && pass "R1.2 applied (the run printed [OK] lines)" || fail "R1.2 printed no [OK] line" +expect_on gaming theme glyphs single launcher notify cec machine poweroff +# R1.3: the state (what vmstate.sh prints) +[[ "$(current_display_manager)" == sddm ]] && [[ -f /etc/sddm.conf.d/10-gamescope-autologin.conf ]] && pass "R1.3 SDDM with autologin" || fail "R1.3 no SDDM autologin" +n=$(compgen -G "/sys/class/leds/valve-leds*" | wc -l); [[ $n -eq 17 ]] && pass "R1.3 17 LED nodes" || fail "R1.3 $n LED nodes" +own=$(stat -c %U /sys/class/leds/valve-leds*/brightness 2>/dev/null | sort -u | tr '\n' ' '); [[ "$own" == "$(id -un) " ]] && pass "R1.3 LEDs owned by the user" || fail "R1.3 LED owner: $own" +[[ $(dkms status | grep -c ': installed') -ge $((3 * $(ls /usr/lib/modules | wc -w))) ]] && pass "R1.3 DKMS installed for every kernel" || fail "R1.3 DKMS: $(dkms status | tr '\n' ';')" +systemctl is-active --quiet steamos-manager && pass "R1.3 steamos-manager active" || systemctl --user is-active --quiet steamos-manager && pass "R1.3 steamos-manager active (user)" || fail "R1.3 steamos-manager not active" +# R1.4: nothing to do +out=$(menu '\nq\n'); grep -qi "already the way you want it" <<< "$out" && pass "R1.4 already the way you want it" || fail "R1.4 message missing" +# R1.5: re-apply: no errors, no duplicates in the journals +out=$(menu 'a\ny\nm\nq\nn\n') +grep -q '^\[ERROR\]' <<< "$out" && fail "R1.5 errors: $(grep -m1 '^\[ERROR\]' <<< "$out")" || pass "R1.5 re-apply without errors" +dup=""; for f in ~/.local/state/steamify/*; do [ -f "$f" ] && d=$(cut -f1-3 "$f" | sort | uniq -d) && [ -n "$d" ] && dup+="$(basename "$f"): $d; "; done +[[ -z "$dup" ]] && pass "R1.5 no duplicates in the journals" || fail "R1.5 DUPLICATES $dup" +# R1.6: --backend status +ver=$(sed -n 's/^VERSION=//p' /mnt/steamify.sh | head -n 1) +js=$(bash /mnt/steamify.sh --backend status 2>/dev/null) +python3 - "$js" "$ver" <<'PY' +import json, sys +def r(ok, m): print(("PASS " if ok else "FAIL ") + m) +try: + d = json.loads(sys.argv[1]); r(True, "R1.6 --backend status is valid JSON") +except Exception as e: + r(False, f"R1.6 --backend status is not JSON: {e}"); sys.exit() +r(sys.argv[2] in json.dumps(d), f"R1.6 the JSON has the current version {sys.argv[2]}") +PY +# R1.10: an item older in features.state shows as an update, and the run brings it up to date +f=~/.local/state/steamify/features.state +[[ -f "$f" ]] && sed -i 's/^cec=.*/cec=2.1.0/' "$f" && grep -q '^cec=2.1.0' "$f" && pass "R1.10 cec set to 2.1.0" || fail "R1.10 cannot set cec older in $f" +out=$(menu 'q\n'); grep -q "HDMI-CEC.*(update)" <<< "$out" && pass "R1.10 the menu shows HDMI-CEC (update)" || fail "R1.10 no (update) on HDMI-CEC" +out=$(menu '\ny\nm\nq\nn\n'); grep -q '^cec=2.1.0' "$f" && fail "R1.10 cec still 2.1.0 after the run" || pass "R1.10 cec brought up to date"