Files
debianito-post-install/tests/desktops.bats
T
stornic56 c94d918757 KDE: add bookworm Wayland gate and release-aware SDDM session probe
Bookworm's plasma-workspace ships only the X11 session; the Wayland
session (plasmawayland.desktop) lives in the separate
plasma-workspace-wayland package (a Recommends of plasma-desktop, now
listed explicitly for the same determinism as the explicit sddm). The
package does not exist on trixie, where plasma-workspace integrates
Wayland natively, so naming it off-bookworm would fail the apt
transaction: resolve it once per installer with _kde_wayland_pkg() and
build the package list in a conditional array so neither the pre-apply
summary nor the apt command ever shows an empty argument. Also correct
the header comment: bookworm still defaults SDDM to the X11 session,
while trixie makes Wayland primary (plasma.desktop in
wayland-sessions) with plasmax11.desktop as the X11 fallback.

Replace the hard-coded SDDM autologin session cascade with an
existence-based ordered probe (_sddm_session_candidates) that prefers
Wayland across releases, keeps the LXQt sessions last for when the
module starts offering LXQt, and reads its session roots from
SESSION_DIR_WAYLAND / SESSION_DIR_X11 for hermetic testing.

Extend tests/desktops.bats with 13 offline tests: gate helper output on
bookworm/trixie, captured apt commands for both installers, summary
content (no empty package line), and the ordered session probe against
fixture trees.
2026-10-21 15:29:26 -05:00

430 lines
14 KiB
Bash

#!/usr/bin/env bash
# desktops.bats — Offline tests for the desktop helpers in
# modules/desktop_display.sh (SP2) and the desktop module
# wiring in _load_module (SP2.5).
#
# No `set -euo pipefail` here: BATS controls error handling
# per test (see utils.bats).
#
# State-mutating functions are called DIRECTLY (a `run`
# subshell would lose their globals); pure-output helpers
# use `run`.
#
# 100% offline: dpkg and whiptail come from the PATH mocks
# in test_helper.bash, systemctl uses the helper mock,
# /etc/X11/default-display-manager is redirected through the
# DM_FILE fixture, and the real audio installer is replaced
# by a call-counter stub.
load test_helper.bash
# Stub for modules/system/audio.sh (never loaded here):
# count calls instead of running apt. Counter reset in setup().
_install_pipewire_standard() {
PIPEWIRE_INSTALL_CALLS=$((PIPEWIRE_INSTALL_CALLS + 1))
}
setup() {
mock_reset
load_utils
# load_utils() sources utils.sh inside a function, so the
# top-level `declare -A _LOADED_MODULES` (utils.sh:48) was
# function-local and vanished when it returned — the same
# caveat the helper documents for the ETH_* arrays.
# Re-declare it globally for the _load_module tests.
declare -gA _LOADED_MODULES=()
# Only function definitions at the top level: safe to source.
# Only function definitions at the top level: safe to source.
source "${TEST_PROJECT_ROOT}/modules/desktop_display.sh"
# The GNOME installers are pure function definitions too;
# desktop_display.sh (their DM/pipewire collaborators) is
# already sourced above.
source "${TEST_PROJECT_ROOT}/modules/desktops/desktop_gnome.sh"
# Fixture redirect for /etc/X11/default-display-manager:
# the file exists but is empty, so `-s` fails and the
# systemctl fallback is exercised deterministically.
DM_FILE="$(mktemp)"
export DM_FILE
PIPEWIRE_INSTALL_CALLS=0
}
teardown() {
if [ -n "${DM_FILE:-}" ]; then
rm -f "$DM_FILE"
fi
if [ -n "${MOCK_SUDO_LOG:-}" ]; then
rm -f "$MOCK_SUDO_LOG"
fi
if [ -n "${MOCK_APT_ROOT:-}" ]; then
rm -rf "$MOCK_APT_ROOT"
fi
if [ -n "${SESSION_FIXTURE:-}" ]; then
rm -rf "$SESSION_FIXTURE"
fi
unset SESSION_DIR_WAYLAND SESSION_DIR_X11
}
# ── _existing_display_manager ────────────────────────
@test "_existing_display_manager reads the default DM file" {
printf '/usr/sbin/gdm3\n' >"$DM_FILE"
run _existing_display_manager
[ "$status" -eq 0 ]
[ "$output" = "gdm3" ]
}
@test "_existing_display_manager prefers the DM file over an enabled unit" {
printf '/usr/sbin/sddm\n' >"$DM_FILE"
export MOCK_SYSTEMCTL_ENABLED="gdm3"
run _existing_display_manager
[ "$status" -eq 0 ]
[ "$output" = "sddm" ]
}
@test "_existing_display_manager falls back to an enabled systemd unit" {
# DM_FILE exists but is empty: `-s` must fail, not `-e`.
export MOCK_SYSTEMCTL_ENABLED="lightdm"
run _existing_display_manager
[ "$status" -eq 0 ]
[ "$output" = "lightdm" ]
}
@test "_existing_display_manager evaluates units in candidate order" {
# gdm3 → lightdm → sddm → greetd → gdm, whatever order
# systemctl reports them in.
export MOCK_SYSTEMCTL_ENABLED="sddm lightdm"
run _existing_display_manager
[ "$status" -eq 0 ]
[ "$output" = "lightdm" ]
}
@test "_existing_display_manager falls back when the DM file is missing" {
rm -f "$DM_FILE"
export MOCK_SYSTEMCTL_ENABLED="greetd"
run _existing_display_manager
[ "$status" -eq 0 ]
[ "$output" = "greetd" ]
}
@test "_existing_display_manager prints nothing when nothing is configured" {
run _existing_display_manager
[ "$status" -eq 0 ]
[ -z "$output" ]
}
# ── _offer_pipewire_after_desktop ────────────────────
@test "_offer_pipewire_after_desktop skips without prompting when pipewire-audio is installed" {
MOCK_DPKG_INSTALLED="pipewire-audio"
WHIPTAIL_CALLS=0
whiptail() { WHIPTAIL_CALLS=$((WHIPTAIL_CALLS + 1)); }
_offer_pipewire_after_desktop
[ "$WHIPTAIL_CALLS" -eq 0 ]
[ "$PIPEWIRE_INSTALL_CALLS" -eq 0 ]
}
@test "_offer_pipewire_after_desktop installs the stack when accepted" {
export MOCK_WHIPTAIL_RC=0 # user answers "Yes"
_offer_pipewire_after_desktop
[ "$PIPEWIRE_INSTALL_CALLS" -eq 1 ]
}
@test "_offer_pipewire_after_desktop returns 0 without installing when declined" {
export MOCK_WHIPTAIL_RC=1 # user answers "No"
offer_rc=0
_offer_pipewire_after_desktop || offer_rc=$?
[ "$offer_rc" -eq 0 ]
[ "$PIPEWIRE_INSTALL_CALLS" -eq 0 ]
}
@test "_offer_pipewire_after_desktop checks the pipewire package on Bullseye" {
export DEBIAN_VERSION="11"
MOCK_DPKG_INSTALLED="pipewire" # Bullseye package name
_offer_pipewire_after_desktop
[ "$PIPEWIRE_INSTALL_CALLS" -eq 0 ]
}
@test "_offer_pipewire_after_desktop ignores pipewire-audio on Bullseye" {
export DEBIAN_VERSION="11"
MOCK_DPKG_INSTALLED="pipewire-audio" # wrong name on 11
export MOCK_WHIPTAIL_RC=0 # user accepts
_offer_pipewire_after_desktop
[ "$PIPEWIRE_INSTALL_CALLS" -eq 1 ]
}
# ── _load_module: desktop wiring (SP2.5) ─────────────
# The dependency cascade is not under test here: mark the
# transitive deps of the desktop modules as loaded so only
# the module file itself is sourced.
_load_module_with_deps_marked() {
local module="$1"
declare -gA _LOADED_MODULES=(
[desktop_display]=1
[audio]=1
[repos]=1
[repo_detect]=1
)
_load_module "$module"
}
@test "_load_module xfce sources modules/desktops/desktop_xfce.sh" {
_load_module_with_deps_marked xfce
type xfce_menu >/dev/null
type _install_xfce_custom >/dev/null
type _xfce_polkit_rules >/dev/null
}
@test "_load_module lxde sources modules/desktops/desktop_lxde.sh" {
_load_module_with_deps_marked lxde
type lxde_menu >/dev/null
type _install_lxde_full >/dev/null
}
@test "_load_module gnome sources modules/desktops/desktop_gnome.sh" {
_load_module_with_deps_marked gnome
type gnome_menu >/dev/null
type _install_gnome_full >/dev/null
}
@test "_load_module kde sources modules/desktops/desktop_kde.sh" {
_load_module_with_deps_marked kde
type kde_menu >/dev/null
type _install_kde_minimal >/dev/null
}
@test "_load_module returns 1 for an unknown module" {
run _load_module no_such_module
[ "$status" -eq 1 ]
}
# ── terminal selection: ptyxis on trixie ──────────────
# gnome-core declares the OR-dep "gnome-terminal or
# gnome-console or ptyxis"; apt resolves an unset OR-dep
# with the first alternative, so listing the terminal
# explicitly in the same apt transaction is what steers
# the resolver. These tests assert the exact package
# that lands in the apt command.
#
# _gnome_capture_apt_cmd runs one installer with every
# external effect stubbed: _run_cmd is replaced so the
# apt command is captured instead of executed, and the
# PATH sudo mock stays in dry-run mode so debconf and
# systemctl invocations are logged, not executed.
_gnome_capture_apt_cmd() {
local installer="$1"
GNOME_APT_CMD=""
_confirm_install_list() { return 0; }
_ensure_apt_updated() { :; }
_run_cmd() { GNOME_APT_CMD="$2"; }
"$installer"
printf '%s\n' "$GNOME_APT_CMD"
}
@test "GNOME Full installs ptyxis on trixie" {
export DEBIAN_CODENAME="trixie"
export MOCK_SUDO_DRY_RUN=1
run _gnome_capture_apt_cmd _install_gnome_full
[ "$status" -eq 0 ]
[[ "$output" == *"apt install -y gnome gdm3 ptyxis"* ]]
[[ "$output" != *gnome-terminal* ]]
}
@test "GNOME Core installs ptyxis on trixie" {
export DEBIAN_CODENAME="trixie"
export MOCK_SUDO_DRY_RUN=1
run _gnome_capture_apt_cmd _install_gnome_core
[ "$status" -eq 0 ]
[[ "$output" == *"apt install -y gnome-core gdm3 ptyxis"* ]]
[[ "$output" != *gnome-terminal* ]]
}
@test "GNOME Full falls back to gnome-terminal off trixie" {
export DEBIAN_CODENAME="bookworm"
export MOCK_SUDO_DRY_RUN=1
run _gnome_capture_apt_cmd _install_gnome_full
[ "$status" -eq 0 ]
[[ "$output" == *"apt install -y gnome gdm3 gnome-terminal"* ]]
[[ "$output" != *ptyxis* ]]
}
@test "GNOME falls back to gnome-terminal when the codename is empty" {
# /etc/os-release missing or without VERSION_CODENAME
# leaves DEBIAN_CODENAME unset: the safe default is the
# historical behaviour.
export DEBIAN_CODENAME=""
export MOCK_SUDO_DRY_RUN=1
run _gnome_capture_apt_cmd _install_gnome_full
[ "$status" -eq 0 ]
[[ "$output" == *"apt install -y gnome gdm3 gnome-terminal"* ]]
[[ "$output" != *ptyxis* ]]
}
# ── KDE Plasma: bookworm Wayland gate ─────────────
# plasma-workspace-wayland exists only on bookworm:
# bookworm's plasma-workspace ships the X11 session
# only, so the explicit listing keeps the Wayland
# session in the transaction deterministically; off
# bookworm the package does not exist and nothing is
# named in the apt command.
# _kde_capture_apt_cmd runs one KDE installer with
# every external effect stubbed (same shape as the
# GNOME capture helper): the apt command is captured
# instead of executed, and the PATH sudo mock stays
# in dry-run mode so debconf and systemctl invocations
# are logged, not executed.
_kde_capture_apt_cmd() {
local installer="$1"
_load_module_with_deps_marked kde
KDE_APT_CMD=""
_confirm_install_list() { return 0; }
_ensure_apt_updated() { :; }
_run_cmd() { KDE_APT_CMD="$2"; }
"$installer"
printf '%s\n' "$KDE_APT_CMD"
}
# _kde_capture_confirm runs one KDE installer with the
# pre-apply summary captured instead of shown: _confirm
# records the list _confirm_install_list builds, so the
# summary content (never an empty package line) can be
# asserted directly. The pipewire follow-up offer is
# stubbed so the recorded list is the package summary.
_kde_capture_confirm() {
local installer="$1"
_load_module_with_deps_marked kde
KDE_CONFIRM_LIST=""
_confirm() { KDE_CONFIRM_LIST="$2"; return 0; }
_ensure_apt_updated() { :; }
_run_cmd() { :; }
_offer_pipewire_after_desktop() { :; }
"$installer"
printf '%s\n' "$KDE_CONFIRM_LIST"
}
@test "_kde_wayland_pkg prints plasma-workspace-wayland on bookworm" {
export DEBIAN_CODENAME="bookworm"
_load_module_with_deps_marked kde
run _kde_wayland_pkg
[ "$status" -eq 0 ]
[ "$output" = "plasma-workspace-wayland" ]
}
@test "_kde_wayland_pkg prints nothing off bookworm" {
export DEBIAN_CODENAME="trixie"
_load_module_with_deps_marked kde
run _kde_wayland_pkg
[ "$status" -eq 0 ]
[ -z "$output" ]
}
@test "KDE Full installs the Wayland session package on bookworm" {
export DEBIAN_CODENAME="bookworm"
export MOCK_SUDO_DRY_RUN=1
run _kde_capture_apt_cmd _install_kde_full
[ "$status" -eq 0 ]
[[ "$output" == *"apt install -y kde-standard sddm plasma-workspace-wayland"* ]]
}
@test "KDE Full omits the Wayland session package off bookworm" {
export DEBIAN_CODENAME="trixie"
export MOCK_SUDO_DRY_RUN=1
run _kde_capture_apt_cmd _install_kde_full
[ "$status" -eq 0 ]
[[ "$output" == *"apt install -y kde-standard sddm"* ]]
[[ "$output" != *"plasma-workspace-wayland"* ]]
}
@test "KDE Minimal installs the Wayland session package on bookworm" {
export DEBIAN_CODENAME="bookworm"
export MOCK_SUDO_DRY_RUN=1
run _kde_capture_apt_cmd _install_kde_minimal
[ "$status" -eq 0 ]
[[ "$output" == *"apt install -y kde-plasma-desktop sddm plasma-workspace-wayland"* ]]
}
@test "KDE Minimal omits the Wayland session package off bookworm" {
export DEBIAN_CODENAME="trixie"
export MOCK_SUDO_DRY_RUN=1
run _kde_capture_apt_cmd _install_kde_minimal
[ "$status" -eq 0 ]
[[ "$output" == *"apt install -y kde-plasma-desktop sddm"* ]]
[[ "$output" != *"plasma-workspace-wayland"* ]]
}
@test "KDE Minimal summary lists the Wayland session package on bookworm" {
export DEBIAN_CODENAME="bookworm"
export MOCK_SUDO_DRY_RUN=1
run _kde_capture_confirm _install_kde_minimal
[ "$status" -eq 0 ]
[[ "$output" == *"plasma-workspace-wayland"* ]]
}
@test "KDE Minimal summary shows no empty package line off bookworm" {
export DEBIAN_CODENAME="trixie"
export MOCK_SUDO_DRY_RUN=1
run _kde_capture_confirm _install_kde_minimal
[ "$status" -eq 0 ]
[[ "$output" == *"kde-plasma-desktop"* ]]
[[ "$output" == *"sddm"* ]]
[[ "$output" != *"plasma-workspace-wayland"* ]]
[[ "$output" != *"- "$'\n'* ]]
}
# ── _sddm_session_candidates: ordered session probe ──
# Existence decides (no codename gates) and Wayland is
# preferred. The roots are redirected to a per-test
# fixture tree through SESSION_DIR_WAYLAND /
# SESSION_DIR_X11 (same spirit as the DM_FILE fixture),
# so the probe never touches the real /usr/share tree.
_sddm_session_fixture() {
SESSION_FIXTURE="$(mktemp -d)"
mkdir -p "$SESSION_FIXTURE/wayland" "$SESSION_FIXTURE/x11"
SESSION_DIR_WAYLAND="$SESSION_FIXTURE/wayland"
SESSION_DIR_X11="$SESSION_FIXTURE/x11"
export SESSION_DIR_WAYLAND SESSION_DIR_X11
}
@test "SDDM session probe prefers bookworm Wayland (plasmawayland)" {
_sddm_session_fixture
: >"$SESSION_DIR_WAYLAND/plasmawayland.desktop"
: >"$SESSION_DIR_X11/plasma.desktop"
run _sddm_session_candidates
[ "$status" -eq 0 ]
[ "$output" = "plasmawayland" ]
}
@test "SDDM session probe picks trixie Wayland (plasma.desktop)" {
_sddm_session_fixture
: >"$SESSION_DIR_WAYLAND/plasma.desktop"
: >"$SESSION_DIR_X11/plasmax11.desktop"
run _sddm_session_candidates
[ "$status" -eq 0 ]
[ "$output" = "plasma" ]
}
@test "SDDM session probe falls back to trixie X11 (plasmax11)" {
_sddm_session_fixture
: >"$SESSION_DIR_X11/plasmax11.desktop"
run _sddm_session_candidates
[ "$status" -eq 0 ]
[ "$output" = "plasmax11" ]
}
@test "SDDM session probe keeps LXQt last when no KDE session exists" {
_sddm_session_fixture
: >"$SESSION_DIR_WAYLAND/lxqt-wayland.desktop"
: >"$SESSION_DIR_X11/lxqt.desktop"
run _sddm_session_candidates
[ "$status" -eq 0 ]
[ "$output" = "lxqt-wayland" ]
}
@test "SDDM session probe returns empty when no session file exists" {
_sddm_session_fixture
run _sddm_session_candidates
[ "$status" -eq 0 ]
[ -z "$output" ]
}