Files
Classroom_Desktop/tests/test-classroom-desktop-wallpaper.sh
T
ekklesiaandClaude Opus 5.5 9fc86f9d4c 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]>
2026-09-29 18:05:54 -05:00

409 lines
14 KiB
Bash
Executable File

#!/bin/bash
# Every desktop shows the classroom's wallpaper, whatever it is numbered.
#
# Tiny Pebbles came through the 0.2.2 upgrade with its desktop as
# containment 10 and no wallpaper configured on it, and showed Plasma's own
# purple one. These run the real repair against that file, against a
# machine that was already right, and against the shapes in between, and
# then check that every place naming the wallpaper names the same one.
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
python3 - "$ROOT" <<'PY'
import importlib.util
import json
import os
import re
import sys
import tempfile
from importlib.machinery import SourceFileLoader
from pathlib import Path
root = Path(sys.argv[1])
loader = SourceFileLoader("wallpaper", str(root / "scripts/classroom-desktop-wallpaper"))
spec = importlib.util.spec_from_loader("wallpaper", loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
passed = failed = 0
def check(label, condition, detail=""):
global passed, failed
if condition:
print(f" ok {label}")
passed += 1
else:
print(f" FAIL {label}")
if detail:
print(f" {detail}")
failed += 1
IMAGE = "Image=file:///usr/share/backgrounds/ekklesia/classroom-calm.png"
def group_texts(text):
"""{group name: its lines}, for saying what did and did not change."""
lines = text.splitlines(keepends=True)
return {
name: "".join(lines[header:end]).rstrip("\n")
for name, header, end in module.groups(lines)
}
def containment_ids(text):
return sorted(re.findall(r"^\[Containments\]\[(\d+)\]$", text, re.M))
# A panel built with no screen present takes id 1 and its applets 2 to 9,
# which is how Tiny Pebbles' desktop came to be 10. Its containment is as
# it was read off the machine.
PANEL_1 = """[ActionPlugins][0]
MiddleButton;NoModifier=org.kde.paste
RightButton;NoModifier=org.kde.contextmenu
[ActionPlugins][1]
RightButton;NoModifier=org.kde.contextmenu
[Containments][1]
activityId=
formfactor=2
immutability=1
lastScreen=0
location=4
plugin=org.kde.panel
wallpaperplugin=org.kde.image
[Containments][1][Applets][2]
immutability=1
plugin=org.kde.plasma.quicklaunch
[Containments][1][Applets][2][Configuration][General]
launcherUrls=file:///usr/share/applications/ekklesia-parent-call.desktop
[Containments][1][Applets][3]
immutability=1
plugin=org.kde.plasma.panelspacer
[Containments][1][Applets][4]
immutability=1
plugin=org.kde.plasma.icontasks
[Containments][1][Applets][4][Configuration][General]
launchers=applications:ekklesia-wonderink.desktop,applications:google-chrome.desktop,applications:ekklesia-dvd.desktop
sortingStrategy=1
[Containments][1][Applets][5]
immutability=1
plugin=org.kde.plasma.panelspacer
[Containments][1][Applets][6]
immutability=1
plugin=org.kde.plasma.networkmanagement
[Containments][1][Applets][7]
immutability=1
plugin=org.kde.plasma.volume
[Containments][1][Applets][8]
immutability=1
plugin=org.kde.plasma.notifications
[Containments][1][Applets][9]
immutability=1
plugin=org.kde.plasma.digitalclock
[Containments][1][General]
AppletOrder=2;3;4;5;6;7;8;9
"""
TINY_PEBBLES = PANEL_1 + """
[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":[]}
[ScreenMapping]
itemsOnDisabledScreens=
"""
# The healthy machine, as the layout builds it with a screen present and
# as the lab's own session wrote it: desktop 1, panel 2, applets 3 to 10.
# The clock is applet 10 — the number that is Tiny Pebbles' desktop.
RIVER_KIDS = """[ActionPlugins][0]
MiddleButton;NoModifier=org.kde.paste
RightButton;NoModifier=org.kde.contextmenu
[Containments][1]
ItemGeometries-1920x1080=
ItemGeometriesHorizontal=
activityId=a9db02f6-926f-4650-b05f-de3a9f0b73fe
formfactor=0
immutability=1
lastScreen=0
location=0
plugin=org.kde.plasma.folder
wallpaperplugin=org.kde.image
[Containments][1][Wallpaper][org.kde.image][General]
FillMode=2
Image=file:///usr/share/backgrounds/ekklesia/classroom-calm.png
SlidePaths=
[Containments][2]
activityId=
formfactor=2
immutability=1
lastScreen=0
location=4
plugin=org.kde.panel
wallpaperplugin=org.kde.image
[Containments][2][Applets][10]
immutability=1
plugin=org.kde.plasma.digitalclock
[Containments][2][Applets][10][Configuration][Appearance]
showDate=true
[Containments][2][Applets][3]
immutability=1
plugin=org.kde.plasma.quicklaunch
[Containments][2][Applets][5]
immutability=1
plugin=org.kde.plasma.icontasks
[Containments][2][General]
AppletOrder=3;4;5;6;7;8;9;10
[ScreenMapping]
itemsOnDisabledScreens=
"""
# What the lab produced when Plasma made a desktop mid-session: the good
# one kept its picture, and the new one, 11, had none.
LATE_DESKTOP = RIVER_KIDS.replace("[ScreenMapping]", """[Containments][11]
activityId=f992593c-2347-4e8d-be82-f80d02e71158
formfactor=0
immutability=1
lastScreen=0
location=0
plugin=org.kde.plasma.folder
wallpaperplugin=org.kde.image
[Containments][11][General]
positions={"1920x1080":[]}
[ScreenMapping]""")
print("1. Tiny Pebbles: a desktop numbered 10, with no wallpaper at all")
check("the desktop is found by what it is, not its number",
module.desktops(TINY_PEBBLES.splitlines(True)) == ["10"],
str(module.desktops(TINY_PEBBLES.splitlines(True))))
after, changes = module.converge(TINY_PEBBLES)
groups_before, groups_after = group_texts(TINY_PEBBLES), group_texts(after)
wallpaper = groups_after.get("[Containments][10][Wallpaper][org.kde.image][General]", "")
check("it gets the classroom's image", IMAGE in wallpaper, wallpaper)
check("scaled and cropped, as the layout writes it", "FillMode=2" in wallpaper)
check("and it says what it did", changes == ["desktop 10: had no wallpaper configured"],
str(changes))
check("the wallpaper plugin is still the image one",
"wallpaperplugin=org.kde.image" in groups_after["[Containments][10]"])
check("one group added and nothing else changed",
set(groups_after) - set(groups_before)
== {"[Containments][10][Wallpaper][org.kde.image][General]"}
and all(groups_after[name] == text for name, text in groups_before.items()))
check("every containment keeps its number", containment_ids(after) == ["1", "10"])
check("the panel is still containment 1, applets 2 to 9, in order",
groups_after["[Containments][1][General]"].endswith("AppletOrder=2;3;4;5;6;7;8;9")
and groups_after["[Containments][1]"] == groups_before["[Containments][1]"])
check("the new group sits with its own containment",
after.index("[Containments][10][General]")
< after.index("[Containments][10][Wallpaper]")
< after.index("[ScreenMapping]"))
again, changes_again = module.converge(after)
check("a second run changes nothing", again == after and changes_again == [], str(changes_again))
print("2. River Kids: already right")
after, changes = module.converge(RIVER_KIDS)
check("the file comes back byte for byte", after == RIVER_KIDS)
check("and nothing is reported as changed", changes == [], str(changes))
check("applet 10 on the panel is not mistaken for a desktop",
module.desktops(RIVER_KIDS.splitlines(True)) == ["1"])
print("3. two desktops, one bare, as the lab made them")
after, changes = module.converge(LATE_DESKTOP)
groups_before, groups_after = group_texts(LATE_DESKTOP), group_texts(after)
check("only the bare one is written",
changes == ["desktop 11: had no wallpaper configured"], str(changes))
check("the good one is untouched",
groups_after["[Containments][1][Wallpaper][org.kde.image][General]"]
== groups_before["[Containments][1][Wallpaper][org.kde.image][General]"])
check("and the panel, numbered 2, is untouched",
all(groups_after[name] == text for name, text in groups_before.items()
if name and name.startswith("[Containments][2]")))
check("the new one has the image",
IMAGE in groups_after.get("[Containments][11][Wallpaper][org.kde.image][General]", ""))
print("4. a desktop some other profile left with another picture")
OTHER = """[Containments][4]
formfactor=0
location=0
plugin=org.kde.desktopcontainment
wallpaperplugin=org.kde.color
[Containments][4][Wallpaper][org.kde.image][General]
Image[$e]=file:///usr/share/wallpapers/Next/contents/images/1920x1080.png
SlidePaths=/usr/share/wallpapers/
[Containments][7]
formfactor=2
location=4
plugin=org.kde.plasma.private.systemtray
[Containments][9]
formfactor=2
location=4
plugin=org.kde.panel
"""
after, changes = module.converge(OTHER)
groups_after = group_texts(after)
check("the plain Desktop containment counts as a desktop",
module.desktops(OTHER.splitlines(True)) == ["4"])
check("its plugin becomes the image one",
"wallpaperplugin=org.kde.image" in groups_after["[Containments][4]"])
wallpaper = groups_after["[Containments][4][Wallpaper][org.kde.image][General]"]
check("its image becomes the classroom's, keeping the key's flags",
"Image[$e]=file:///usr/share/backgrounds/ekklesia/classroom-calm.png" in wallpaper,
wallpaper)
check("a missing key is added to the existing group", "FillMode=2" in wallpaper)
check("a key the classroom does not set is left alone",
"SlidePaths=/usr/share/wallpapers/" in wallpaper)
check("a system tray is not a desktop",
groups_after["[Containments][7]"] == group_texts(OTHER)["[Containments][7]"])
check("nor is a panel",
groups_after["[Containments][9]"] == group_texts(OTHER)["[Containments][9]"])
check("and it settles", module.converge(after) == (after, []))
print("5. the command, on files")
work = Path(tempfile.mkdtemp())
def run(*args):
return module.main([str(arg) for arg in args])
target = work / "tiny-pebbles"
target.write_text(TINY_PEBBLES)
os.chmod(target, 0o600)
check("--check says it is wrong", run("--check", target) == 1)
check("and changes nothing", target.read_text() == TINY_PEBBLES)
check("the repair succeeds", run(target) == 0)
check("the file is repaired", IMAGE in target.read_text())
check("the file keeps its permissions", (target.stat().st_mode & 0o777) == 0o600)
check("no temporary file is left behind", sorted(p.name for p in work.iterdir()) == ["tiny-pebbles"])
stamp = (target.stat().st_ino, target.stat().st_mtime_ns)
check("a second run succeeds", run(target) == 0)
check("and does not write the file at all",
(target.stat().st_ino, target.stat().st_mtime_ns) == stamp)
check("--check then says it is right", run("--check", target) == 0)
healthy = work / "river-kids"
healthy.write_text(RIVER_KIDS)
stamp = (healthy.stat().st_ino, healthy.stat().st_mtime_ns)
check("a machine that is right is left alone", run(healthy) == 0
and (healthy.stat().st_ino, healthy.stat().st_mtime_ns) == stamp)
check("no file at all is not an error", run(work / "absent") == 0)
check("and none is created", not (work / "absent").exists())
empty = work / "no-desktop"
empty.write_text(PANEL_1)
check("a file with only a panel is not an error", run(empty) == 0)
check("and is not changed", empty.read_text() == PANEL_1)
print("6. never underneath a running shell")
config_home = work / "config"
config_home.mkdir()
live = config_home / module.APPLETSRC
live.write_text(TINY_PEBBLES)
os.environ["XDG_CONFIG_HOME"] = str(config_home)
check("the default file is the one in XDG_CONFIG_HOME", module.default_path() == str(live))
proc = work / "proc"
for pid, name in (("1", "systemd"), ("812", "kwin_wayland"), ("self", "python3")):
(proc / pid).mkdir(parents=True)
(proc / pid / "comm").write_text(name + "\n")
check("no plasmashell in the process table is no shell",
module.shell_running(str(proc)) is False)
(proc / "1044").mkdir()
(proc / "1044" / "comm").write_text("plasmashell\n")
check("this user's plasmashell is found", module.shell_running(str(proc)) is True)
check("and the real process table can be read",
module.shell_running() in (True, False))
module.shell_running = lambda: True
check("with plasmashell running it refuses", run() == 1)
check("and the shell's file is untouched", live.read_text() == TINY_PEBBLES)
check("--check still answers while it runs", run("--check") == 1)
module.shell_running = lambda: False
check("with the shell stopped it repairs", run() == 0 and IMAGE in live.read_text())
print("7. one wallpaper, named the same everywhere")
layout = (root / "config/plasma/lookandfeel/contents/layouts/org.kde.plasma.desktop-layout.js").read_text()
defaults = (root / "config/plasma/lookandfeel/contents/defaults").read_text()
build = (root / "tools/build-release.sh").read_text()
dropin = (root / "systemd/user/plasma-plasmashell.service.d/ekklesia-classroom.conf").read_text()
package = root / "config/wallpapers/EkklesiaClassroom"
metadata = json.loads((package / "metadata.json").read_text())
check("the layout and the repair name the same file",
f'var WALLPAPER = "{module.WALLPAPER}";' in layout)
check("and the same fill mode", 'desktop.writeConfig("FillMode", 2);' in layout
and ("FillMode", "2") in module.WALLPAPER_KEYS)
check("the packaged file is where they say", f'"$STAGE{module.WALLPAPER}"' in build)
default = re.search(r"^\[Wallpaper\]\nImage=(.*)$", defaults, re.M)
check("the look and feel's default is a package name, not a path",
default is not None and "/" not in default.group(1),
default.group(1) if default else "no [Wallpaper] Image")
check("the package it names is the classroom's",
default is not None and default.group(1) == metadata["KPlugin"]["Id"] == package.name)
check("the build installs that package's metadata",
f'"$ROOT/config/wallpapers/{package.name}/metadata.json"' in build
and f'WALLPAPER_PACKAGE="$STAGE/usr/share/wallpapers/{package.name}"' in build)
check("its image is a link to the one file, not a copy",
f"ln -s {module.WALLPAPER}" in build)
check("the repair is packaged",
'"$STAGE/usr/local/bin/classroom-desktop-wallpaper"' in build)
check("it runs before every shell start",
"ExecStartPre=-/usr/local/bin/classroom-desktop-wallpaper" in dropin)
check("and a failure in it cannot stop the shell",
not re.search(r"^ExecStartPre=/", dropin, re.M))
check("the layout still writes the wallpaper for a fresh desktop",
'desktop.writeConfig("Image", "file://" + WALLPAPER);' in layout)
print()
print(f"{passed} passed, {failed} failed")
sys.exit(1 if failed else 0)
PY