test: the wallpaper repair, on Tiny Pebbles' own file, in the lab and on a real install

Unit: Tiny Pebbles' file (desktop 10, panel 1), a healthy River Kids file
left byte for byte, a late desktop beside a good one, idempotence, the
refusal under a running shell. Lab: Plasma makes a bare desktop itself
through a new activity, and the screen is read, not the file. Container
gate: the installed package's fallback is the package, and its repair
fixes the room's file and leaves the panel alone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ekklesia
2026-09-29 18:05:54 -05:00
co-authored by Claude Opus 5.5
parent 7b4ac39cd2
commit 9fc86f9d4c
4 changed files with 717 additions and 0 deletions
+6
View File
@@ -182,6 +182,12 @@ check "no discover or system settings" 0 \
"$(in_lab "grep -cE 'discover.desktop|systemsettings.desktop' $APPLETS")"
check "the packaged wallpaper" 1 \
"$(in_lab "grep -c 'Image=file:///usr/share/backgrounds/ekklesia/classroom-calm.png' $APPLETS")"
# And the one a desktop gets when nothing wrote it, which is what Tiny
# Pebbles showed: a package name, and the package there to be found.
check "a desktop nobody configured defaults to it" EkklesiaClassroom \
"$(in_lab 'sed -n "/^\[Wallpaper\]/,/^\[/s/^Image=//p" /usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/defaults')"
check "and that package is the same picture" /usr/share/backgrounds/ekklesia/classroom-calm.png \
"$(in_lab 'readlink -f /usr/share/wallpapers/EkklesiaClassroom/contents/images/*.png')"
section "it will not strand a teacher"
check "no automatic lock" false "$(session_config --file kscreenlockerrc --group Daemon --key Autolock)"
+238
View File
@@ -0,0 +1,238 @@
#!/bin/bash
# Does every desktop show the classroom's wallpaper — including one Plasma
# makes by itself after the layout has run?
#
# Tiny Pebbles came through the 0.2.2 upgrade with its desktop numbered 10
# and nothing configured on it, and showed Plasma's own purple wallpaper.
# The layout only reaches the desktops that exist while it runs, and a
# room whose updater reboots it at three in the morning with the
# television off has none then. The lab cannot switch a television on, so
# it makes Plasma create a desktop the other way it does, through a new
# activity: the same code in the shell, and measured to give the same
# bare containment.
#
# tools/lab/wallpaper-check against the running lab session
#
# Each answer is read off the screen, not out of a file: a configuration
# can say anything, and the question is what the room sees. The sky at the
# top of the classroom's picture is pale blue; Plasma's is purple.
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
LAB="$ROOT/tools/lab/lab"
NAME="${LAB_NAME:-classroom-v2-lab}"
APPLETS=/home/classroom/.config/plasma-org.kde.plasma.desktop-appletsrc
PASS=0
FAIL=0
check() {
if [[ "$2" == "$3" ]]; then
printf ' \033[32mPASS\033[0m %-58s %s\n' "$1" "$3"
PASS=$((PASS + 1))
else
printf ' \033[31mFAIL\033[0m %-58s got [%s] want [%s]\n' "$1" "$3" "$2"
FAIL=$((FAIL + 1))
fi
}
section() { printf '\n%s\n' "$1"; }
in_lab() { docker exec "$NAME" bash -c "$1" 2>/dev/null; }
# The same, with this script's input handed through — for a program on a
# here-document. Without -i docker gives it nothing to read, and it runs
# on nothing and says nothing.
in_lab_fed() { docker exec -i "$NAME" bash -c "$1"; }
user() { "$LAB" run "$@" 2>/dev/null; }
activities() {
user busctl --user call org.kde.ActivityManager /ActivityManager/Activities \
org.kde.ActivityManager.Activities "$@"
}
# What is on the screen at the top middle, where the classroom's picture
# is sky and Plasma's is purple: "classroom", "plasma", or the colour.
screen() {
"$LAB" outer grim -t ppm - 2>/dev/null | python3 -c '
import sys
data = sys.stdin.buffer.read()
fields, at = [], 0
while len(fields) < 4:
while data[at:at + 1].isspace():
at += 1
end = at
while not data[end:end + 1].isspace():
end += 1
fields.append(data[at:end])
at = end
width, height = int(fields[1]), int(fields[2])
pixels = data[at + 1:]
total = [0, 0, 0]
count = 0
for y in range(20, 140, 4):
for x in range(width * 2 // 5, width * 3 // 5, 8):
offset = (y * width + x) * 3
for channel in range(3):
total[channel] += pixels[offset + channel]
count += 1
r, g, b = (value / count for value in total)
if b > r + 25 and g > r + 10:
print("classroom")
elif r > g + 25 and b > g + 25:
print("plasma")
else:
print(f"rgb({r:.0f},{g:.0f},{b:.0f})")
'
}
# The shell writes its file a few seconds after a change, not at once.
settle() { sleep "${1:-35}"; }
# The shell's file, read as groups rather than grepped, so nothing here
# can pick a line out of the wrong containment.
#
# applets panels each panel's number, its applets' numbers and plugins
# and their order. Not the panel's text: the shell adds
# keys of its own to applets as soon as it runs (popup
# sizes, the volume's migration), and that is Plasma,
# not the repair. tests/ checks the repair's own
# output byte for byte.
# applets desktops the desktop containments as "id:image" or "id:none",
# found by plugin and never by number
applets() {
in_lab_fed "python3 - $1 $APPLETS" <<'PY'
import re, sys
mode, path = sys.argv[1], sys.argv[2]
groups, name = {}, None
for line in open(path).read().splitlines():
if line.startswith("[") and line.endswith("]"):
name = line
groups[name] = []
elif name and line:
groups[name].append(line)
def top(cid):
return groups.get(f"[Containments][{cid}]", [])
ids = [m.group(1) for g in groups for m in [re.match(r"^\[Containments\]\[(\d+)\]$", g)] if m]
if mode == "panels":
for c in ids:
if "plugin=org.kde.panel" not in top(c):
continue
applets = sorted(
(int(m.group(1)), line[len("plugin=org.kde.plasma."):])
for g in groups
for m in [re.match(r"^\[Containments\]\[%s\]\[Applets\]\[(\d+)\]$" % c, g)] if m
for line in groups[g] if line.startswith("plugin=")
)
order = [l[len("AppletOrder="):] for l in groups.get(f"[Containments][{c}][General]", [])
if l.startswith("AppletOrder=")]
print(f"panel {c}:", " ".join(f"{n}={name}" for n, name in applets), "order", *order)
else:
image = "Image=file:///usr/share/backgrounds/ekklesia/classroom-calm.png"
out = []
for c in ids:
if {"plugin=org.kde.plasma.folder", "plugin=org.kde.desktopcontainment"} & set(top(c)):
wallpaper = groups.get(f"[Containments][{c}][Wallpaper][org.kde.image][General]", [])
out.append(f"{c}:{'image' if image in wallpaper else 'none'}")
print(" ".join(out))
PY
}
panel_groups() { applets panels; }
desktops() { applets desktops; }
last_repair() {
user journalctl --user -u plasma-plasmashell.service --no-pager -o cat \
| grep '^classroom-desktop-wallpaper:' | tail -1
}
restart_shell() {
user systemctl --user restart plasma-plasmashell.service
for _ in $(seq 1 30); do
in_lab 'pgrep -x plasmashell >/dev/null' && break
sleep 1
done
sleep 10
}
section "the package"
check "the look and feel's default names a package" EkklesiaClassroom \
"$(in_lab 'sed -n "/^\[Wallpaper\]/,/^\[/s/^Image=//p" /usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/defaults')"
check "which is installed, and is the classroom's picture" /usr/share/backgrounds/ekklesia/classroom-calm.png \
"$(in_lab 'readlink -f /usr/share/wallpapers/EkklesiaClassroom/contents/images/*.png')"
check "the repair runs before every shell start" 1 \
"$(in_lab 'grep -c "^ExecStartPre=-/usr/local/bin/classroom-desktop-wallpaper" /etc/systemd/user/plasma-plasmashell.service.d/ekklesia-classroom.conf')"
section "the session as it came up"
check "the room shows the classroom's picture" classroom "$(screen)"
original=$(activities CurrentActivity | cut -d'"' -f2)
section "a desktop Plasma makes by itself (Tiny Pebbles' television)"
panels_before=$(panel_groups)
late=$(activities AddActivity s "Late screen" | cut -d'"' -f2)
activities SetCurrentActivity s "$late" >/dev/null
settle
now=$(desktops)
check "Plasma made a second desktop" 2 "$(wc -w <<< "$now")"
check "and wrote no wallpaper into it, as on Tiny Pebbles" 1 "$(grep -o ':none' <<< "$now" | wc -l)"
check "the room still shows the classroom's picture" classroom "$(screen)"
section "the shell restarted, without a login"
restart_shell
check "the repair wrote it in" "classroom-desktop-wallpaper: the wallpaper is now configured on desktop $(tr ' ' '\n' <<< "$now" | cut -d: -f1 | paste -sd, | sed 's/,/, /g')" \
"$(last_repair)"
check "every desktop now names the picture" 0 "$(desktops | grep -o ':none' | wc -l)"
check "the panel keeps its numbers, applets and order" "$panels_before" "$(panel_groups)"
check "the room shows the classroom's picture" classroom "$(screen)"
restart_shell
check "a second restart finds nothing to do" \
"classroom-desktop-wallpaper: desktop $(desktops | tr ' ' '\n' | cut -d: -f1 | paste -sd, | sed 's/,/, /g'): the classroom wallpaper already" \
"$(last_repair)"
check "and still does" "$panels_before" "$(panel_groups)"
activities SetCurrentActivity s "$original" >/dev/null
activities RemoveActivity s "$late" >/dev/null
sleep 3
section "Tiny Pebbles' own file: desktop 10, panel 1, no wallpaper"
user systemctl --user stop plasma-plasmashell.service
in_lab_fed "python3 - '$original' $APPLETS" <<'PY'
import sys
activity, path = sys.argv[1], sys.argv[2]
applets = ["quicklaunch", "panelspacer", "icontasks", "panelspacer",
"networkmanagement", "volume", "notifications", "digitalclock"]
out = ["[Containments][1]", "activityId=", "formfactor=2", "immutability=1",
"lastScreen=0", "location=4", "plugin=org.kde.panel",
"wallpaperplugin=org.kde.image", ""]
for number, applet in enumerate(applets, start=2):
out += [f"[Containments][1][Applets][{number}]", "immutability=1",
f"plugin=org.kde.plasma.{applet}", ""]
out += ["[Containments][1][Applets][4][Configuration][General]",
"launchers=applications:ekklesia-wonderink.desktop,applications:google-chrome.desktop,applications:ekklesia-dvd.desktop",
"sortingStrategy=1", "",
"[Containments][1][General]", "AppletOrder=2;3;4;5;6;7;8;9", "",
"[Containments][10]", f"activityId={activity}", "formfactor=0",
"immutability=1", "lastScreen=0", "location=0",
"plugin=org.kde.plasma.folder", "wallpaperplugin=org.kde.image", "",
"[Containments][10][General]", 'positions={"1920x1080":[]}', ""]
open(path, "w").write("\n".join(out))
PY
in_lab "chown classroom:classroom $APPLETS"
panels_before=$(panel_groups)
user systemctl --user start plasma-plasmashell.service
sleep 12
check "the repair found desktop 10 and wrote it" \
"classroom-desktop-wallpaper: the wallpaper is now configured on desktop 10" "$(last_repair)"
check "desktop 10 names the picture" "10:image" "$(desktops)"
check "the panel is still containment 1, applets 2 to 9, in order" "$panels_before" "$(panel_groups)"
check "the room shows the classroom's picture" classroom "$(screen)"
section "and at the next login"
"$LAB" plasma-down >/dev/null
"$LAB" plasma-up >/dev/null
settle 10
check "the reset left the repair nothing to do" \
"classroom-desktop-wallpaper: no desktop configuration yet; the layout writes the wallpaper" \
"$(last_repair)"
check "the layout rebuilt one desktop with the picture" 1 "$(desktops | grep -o ':image' | wc -l)"
check "the room shows the classroom's picture" classroom "$(screen)"
printf '\n%s passed, %s failed\n' "$PASS" "$FAIL"
[[ "$FAIL" -eq 0 ]]
+65
View File
@@ -499,6 +499,71 @@ if [[ -n "$previous" ]]; then
grep -q 'var PRESENTATIONS = false;' \
/usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/layouts/org.kde.plasma.desktop-layout.js
# TINY PEBBLES, 0.2.2 -> 0.2.6.4: THE DESKTOP WITH NO WALLPAPER.
#
# The room's desktop was containment 10, made by Plasma after the
# layout had run, with nothing configured on it; it showed Plasma's
# own wallpaper. What a desktop gets when nothing wrote it is the look
# and feel's default, and that has to be a package Plasma can find.
lnf=/usr/share/plasma/look-and-feel/org.ekklesia.classroom.desktop/contents/defaults
[[ "$(sed -n '/^\[Wallpaper\]/,/^\[/s/^Image=//p' "$lnf")" == EkklesiaClassroom ]]
grep -q '"Id": "EkklesiaClassroom"' /usr/share/wallpapers/EkklesiaClassroom/metadata.json
[[ "$(readlink -f /usr/share/wallpapers/EkklesiaClassroom/contents/images/*.png)" \
== /usr/share/backgrounds/ekklesia/classroom-calm.png ]]
grep -qx 'ExecStartPre=-/usr/local/bin/classroom-desktop-wallpaper' \
/etc/systemd/user/plasma-plasmashell.service.d/ekklesia-classroom.conf
# And the repair the package installed, on that room's own file: the
# panel built with no screen took 1 and its applets 2 to 9.
wallpaper_home=$(mktemp -d)
cat > "$wallpaper_home/plasma-org.kde.plasma.desktop-appletsrc" <<'APPLETS'
[Containments][1]
formfactor=2
location=4
plugin=org.kde.panel
wallpaperplugin=org.kde.image
[Containments][1][Applets][2]
plugin=org.kde.plasma.quicklaunch
[Containments][1][Applets][9]
plugin=org.kde.plasma.digitalclock
[Containments][1][General]
AppletOrder=2;3;4;5;6;7;8;9
[Containments][10]
activityId=62d3d55d-bd31-4fc8-9a7b-a5b9b2b99b60
formfactor=0
immutability=1
lastScreen=0
location=0
plugin=org.kde.plasma.folder
wallpaperplugin=org.kde.image
[Containments][10][General]
positions={"1920x1080":[]}
APPLETS
applets="$wallpaper_home/plasma-org.kde.plasma.desktop-appletsrc"
panel_before=$(sed -n '/^\[Containments\]\[1\]/,/^$/p' "$applets")
! XDG_CONFIG_HOME="$wallpaper_home" classroom-desktop-wallpaper --check >/dev/null
XDG_CONFIG_HOME="$wallpaper_home" classroom-desktop-wallpaper \
| grep -q 'configured on desktop 10'
sed -n '/^\[Containments\]\[10\]\[Wallpaper\]\[org.kde.image\]\[General\]$/,/^$/p' "$applets" \
| grep -qx 'Image=file:///usr/share/backgrounds/ekklesia/classroom-calm.png'
[[ "$(sed -n '/^\[Containments\]\[1\]/,/^$/p' "$applets")" == "$panel_before" ]]
[[ "$(grep -c '^\[Containments\]\[[0-9]*\]$' "$applets")" -eq 2 ]]
# Twice changes nothing, and a room that was already right is not
# written at all.
repaired=$(sha256sum < "$applets")
XDG_CONFIG_HOME="$wallpaper_home" classroom-desktop-wallpaper \
| grep -q 'the classroom wallpaper already'
[[ "$(sha256sum < "$applets")" == "$repaired" ]]
XDG_CONFIG_HOME="$wallpaper_home" classroom-desktop-wallpaper --check >/dev/null
rm -rf "$wallpaper_home"
# And a room that was carried through the experiment on its own
# channel comes home, because that channel is the obsolete one now.
sed -i 's/^CHANNEL=.*/CHANNEL=classroom-v2/' /etc/ekklesia-classroom/device.conf