build: keep the DVD and MQTT helpers, and count the deletion honestly
Building the package showed what reading the tree did not: classroom-dvd still calls classroom-window, classroom-mqtt still calls classroom-notify, and the DVD overlay still imports the icon and stylesheet helpers the dock shared. They go back in, and the write-up now counts what was actually removed rather than what could be. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6e4786f448
commit
4c3f19d599
@@ -1,280 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -uo pipefail
|
||||
|
||||
# Secondary Chrome windows: which window is which, how a popup is placed,
|
||||
# and which one the dock offers to close.
|
||||
#
|
||||
# tests/test-classroom-chrome-windows.sh
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
|
||||
PYTHONPATH="$ROOT/lib" python3 - "$ROOT" <<'PY'
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from classroom_chrome import ChromeRoles, chrome_windows
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
path = root / "scripts/classroom-chrome-windows"
|
||||
loader = importlib.machinery.SourceFileLoader("classroom_chrome_windows", str(path))
|
||||
spec = importlib.util.spec_from_loader(loader.name, loader)
|
||||
script = importlib.util.module_from_spec(spec)
|
||||
loader.exec_module(script)
|
||||
|
||||
source = path.read_text()
|
||||
dock_source = (root / "scripts/classroom-dock").read_text()
|
||||
|
||||
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
|
||||
|
||||
|
||||
def window(identifier, title, floating=False, hidden=False, focused=False):
|
||||
return {
|
||||
"id": identifier,
|
||||
"app_id": "google-chrome",
|
||||
"class": "",
|
||||
"instance": "",
|
||||
"pid": 4242,
|
||||
"name": title,
|
||||
"focused": focused,
|
||||
"floating": floating,
|
||||
"hidden": hidden,
|
||||
}
|
||||
|
||||
|
||||
print("1. creation order decides which window is the browser")
|
||||
roles = ChromeRoles()
|
||||
main = window(5, "Main browser - Google Chrome")
|
||||
popup = window(8, "about:blank - Google Chrome")
|
||||
|
||||
change = roles.update([main])
|
||||
check("the first Chrome window is the browser", change["primary"] == 5)
|
||||
check("it is not a popup", change["new_secondary"] == [])
|
||||
|
||||
change = roles.update([main, popup])
|
||||
check("a later Chrome window is a popup", change["new_secondary"] == [8])
|
||||
check("the browser stays the browser", change["primary"] == 5)
|
||||
|
||||
renamed_main = window(5, "Something else entirely")
|
||||
renamed_popup = window(8, "A different page title")
|
||||
change = roles.update([renamed_main, renamed_popup])
|
||||
check("titles never change a role",
|
||||
change["primary"] == 5 and not change["new_secondary"])
|
||||
check("a shared PID never confuses roles",
|
||||
len(chrome_windows([renamed_main, renamed_popup])) == 2)
|
||||
|
||||
print()
|
||||
print("2. several popups at once")
|
||||
third = window(9, "Third window")
|
||||
change = roles.update([main, popup, third])
|
||||
check("a third top-level is also a popup", change["new_secondary"] == [9])
|
||||
change = roles.update([main, popup])
|
||||
check("closing one leaves the other a popup", roles.secondary == [8])
|
||||
check("the closed one is reported", change["removed"] == [9])
|
||||
|
||||
print()
|
||||
print("3. how a popup is placed")
|
||||
commands = []
|
||||
script.command = lambda argument: commands.append(argument)
|
||||
|
||||
script.SETTLE_SECONDS = 0
|
||||
script.configure_secondary(8)
|
||||
check("floats exactly one container", commands[0] == "[con_id=8] floating enable")
|
||||
check("borderless, like the main browser", commands[1] == "[con_id=8] border none")
|
||||
check("about three quarters of the output",
|
||||
"width 76 ppt height 76 ppt" in commands[2], commands)
|
||||
check("centred", commands[3] == "[con_id=8] move position center")
|
||||
check("focused", commands[4] == "[con_id=8] focus")
|
||||
# Chrome resizes a popup after the fact, so the placement is repeated
|
||||
# once the size has settled or the window ends up off to one side.
|
||||
check("placed again after Chrome settles",
|
||||
commands[-1] == "[con_id=8] move position center"
|
||||
and "width 76 ppt" in commands[-2], commands)
|
||||
check("placed exactly twice, never on a loop",
|
||||
commands.count("[con_id=8] move position center") == 2, commands)
|
||||
check("never matches all Chrome windows at once",
|
||||
all("app_id" not in item for item in commands))
|
||||
|
||||
commands.clear()
|
||||
script.configure_secondary(8, arriving=False)
|
||||
check("restoring the role never moves the window",
|
||||
not any("move position" in item for item in commands), commands)
|
||||
check("restoring the role never raises it over a dialog",
|
||||
not any("focus" in item for item in commands), commands)
|
||||
check("it only puts the window back to floating and borderless",
|
||||
commands == ["[con_id=8] floating enable", "[con_id=8] border none"],
|
||||
commands)
|
||||
|
||||
commands.clear()
|
||||
script.configure_primary(8)
|
||||
check("a promoted window goes back to tiled",
|
||||
commands[0] == "[con_id=8] floating disable")
|
||||
check("and borderless", commands[1] == "[con_id=8] border none")
|
||||
|
||||
print()
|
||||
print("4. the dock's Close target survives losing focus")
|
||||
# The whole point: the pointer has to travel from the popup down to the
|
||||
# dock, and that moves focus. If the target followed focus the button
|
||||
# would vanish before anyone could reach it.
|
||||
active = script.ActiveSecondary()
|
||||
|
||||
chrome = {5: window(5, "main", focused=True)}
|
||||
check("nothing to close with only the main browser",
|
||||
active.update([], chrome) is None)
|
||||
|
||||
chrome = {5: window(5, "main"), 8: window(8, "popup", focused=True)}
|
||||
check("a focused popup becomes the target", active.update([8], chrome) == 8)
|
||||
|
||||
chrome = {5: window(5, "main"), 8: window(8, "popup")}
|
||||
check("it stays the target when nothing is focused",
|
||||
active.update([8], chrome) == 8)
|
||||
|
||||
chrome = {5: window(5, "main", focused=True), 8: window(8, "popup")}
|
||||
check("it stays when focus moves to the main browser",
|
||||
active.update([8], chrome) == 8)
|
||||
check("the main browser is never the target", active.target != 5)
|
||||
|
||||
chrome = {5: window(5, "main"), 8: window(8, "popup"), 9: window(9, "dialog", focused=True)}
|
||||
check("a newer focused popup takes over", active.update([8, 9], chrome) == 9)
|
||||
|
||||
chrome = {5: window(5, "main"), 8: window(8, "popup"), 9: window(9, "dialog")}
|
||||
check("and keeps it after focus moves to the dock",
|
||||
active.update([8, 9], chrome) == 9)
|
||||
|
||||
chrome = {5: window(5, "main"), 8: window(8, "popup")}
|
||||
check("closing the dialog falls back to the render window",
|
||||
active.update([8], chrome) == 8)
|
||||
|
||||
chrome = {5: window(5, "main")}
|
||||
check("closing the last popup clears it", active.update([], chrome) is None)
|
||||
|
||||
fresh = script.ActiveSecondary()
|
||||
chrome = {5: window(5, "main", focused=True), 8: window(8, "popup")}
|
||||
check("an unfocused popup is still closeable",
|
||||
fresh.update([8], chrome) == 8)
|
||||
|
||||
hidden = script.ActiveSecondary()
|
||||
chrome = {8: window(8, "one", hidden=True)}
|
||||
check("a minimised popup is not adopted",
|
||||
hidden.update([8], chrome) is None)
|
||||
|
||||
print()
|
||||
print("4b. the print dialog stays in front")
|
||||
|
||||
def win(identifier, title, focused=False, hidden=False):
|
||||
w = window(identifier, title, focused=focused, hidden=hidden)
|
||||
w["name"] = title
|
||||
return w
|
||||
|
||||
check("the dialog is recognised by its exact title",
|
||||
script.is_print_dialog({"name": "Print"}))
|
||||
check("the render window is not",
|
||||
not script.is_print_dialog({"name": "Printable Document - Google Chrome"}))
|
||||
check("nothing else is", not script.is_print_dialog({"name": "Print Preview"}))
|
||||
check("a missing title is not", not script.is_print_dialog({}))
|
||||
|
||||
commands.clear()
|
||||
# The render window has raised itself over the dialog.
|
||||
chrome = {5: win(5, "main"), 8: win(8, "Printable Document", focused=True),
|
||||
9: win(9, "Print")}
|
||||
script.keep_print_in_front([8, 9], chrome)
|
||||
check("focus is taken back for the dialog",
|
||||
commands == ["[con_id=9] focus"], commands)
|
||||
|
||||
commands.clear()
|
||||
chrome = {5: win(5, "main"), 8: win(8, "Printable Document"), 9: win(9, "Print", focused=True)}
|
||||
script.keep_print_in_front([8, 9], chrome)
|
||||
check("nothing happens while the dialog already has focus", commands == [], commands)
|
||||
|
||||
commands.clear()
|
||||
chrome = {5: win(5, "main", focused=True), 8: win(8, "Printable Document"),
|
||||
9: win(9, "Print")}
|
||||
script.keep_print_in_front([8, 9], chrome)
|
||||
check("focus is never stolen from the main browser", commands == [], commands)
|
||||
|
||||
commands.clear()
|
||||
chrome = {5: win(5, "main"), 8: win(8, "Printable Document")}
|
||||
script.keep_print_in_front([8], chrome)
|
||||
check("nothing happens when there is no dialog", commands == [], commands)
|
||||
|
||||
commands.clear()
|
||||
chrome = {8: win(8, "Printable Document", focused=True), 9: win(9, "Print", hidden=True)}
|
||||
script.keep_print_in_front([8, 9], chrome)
|
||||
check("a minimised dialog is left alone", commands == [], commands)
|
||||
|
||||
# Both arriving together: whichever is configured last ends up in front,
|
||||
# so the dialog has to be last.
|
||||
order = sorted([8, 9], key=lambda i: script.is_print_dialog(
|
||||
{8: {"name": "Printable Document"}, 9: {"name": "Print"}}[i]))
|
||||
check("the dialog is configured after the render window", order == [8, 9], order)
|
||||
order = sorted([9, 8], key=lambda i: script.is_print_dialog(
|
||||
{8: {"name": "Printable Document"}, 9: {"name": "Print"}}[i]))
|
||||
check("even when the tree lists it first", order == [8, 9], order)
|
||||
|
||||
print()
|
||||
print("5. the state the dock reads")
|
||||
work = Path(tempfile.mkdtemp())
|
||||
state = work / "chrome-close-target"
|
||||
|
||||
script.publish(9, state)
|
||||
check("publishes the container id", state.read_text().strip() == "9")
|
||||
check("published by rename, not left half written",
|
||||
not (work / "chrome-close-target.tmp").exists())
|
||||
|
||||
script.publish(None, state)
|
||||
check("empty when there is nothing to close", state.read_text().strip() == "")
|
||||
|
||||
print()
|
||||
print("6. the dock owns the button, not the window")
|
||||
check("no layer-shell surface in the window helper",
|
||||
"GtkLayerShell" not in source)
|
||||
check("no second GTK process spawned for a button",
|
||||
"Gtk.main" not in source and "show_close_button" not in source)
|
||||
check("no geometry maths chasing a window",
|
||||
"output_rect" not in source and "button_margins" not in source)
|
||||
check("the dock has a close action", '"close"' in dock_source)
|
||||
check("the dock closes exactly one container",
|
||||
'[con_id={self.close_target}] kill' in dock_source)
|
||||
check("the dock never kills Chrome processes",
|
||||
"pkill" not in dock_source and "killall" not in dock_source)
|
||||
check("the dock watches rather than polls for it",
|
||||
"monitor_directory" in dock_source)
|
||||
|
||||
print()
|
||||
print("7. driven by Sway events, not a poll")
|
||||
check("subscribes to window events", '"-t", "subscribe"' in source)
|
||||
check("ignores title churn",
|
||||
"title" not in script.INTERESTING and "new" in script.INTERESTING)
|
||||
check("reacts to focus and movement",
|
||||
{"focus", "move", "close"} <= set(script.INTERESTING))
|
||||
check("no polling loop constant", "POLL_SECONDS" not in source)
|
||||
|
||||
print()
|
||||
print("8. the session still starts it")
|
||||
config = (root / "config/sway/config").read_text()
|
||||
check("started with the session",
|
||||
"exec --no-startup-id /usr/local/bin/classroom-chrome-windows" in config)
|
||||
check("no broad Chrome floating rule",
|
||||
not any("floating enable" in line and "google-chrome" in line
|
||||
for line in config.splitlines()))
|
||||
|
||||
print()
|
||||
print(f"passed: {passed} failed: {failed}")
|
||||
sys.exit(1 if failed else 0)
|
||||
PY
|
||||
@@ -3,7 +3,7 @@ set -uo pipefail
|
||||
|
||||
# Tests the session resolution in classroom-notify-test.
|
||||
#
|
||||
# sway is stood in for by a real background process started with a known
|
||||
# The compositor is stood in for by a real background process started with a known
|
||||
# environment, so /proc/<pid>/environ is genuine rather than mocked. id,
|
||||
# pgrep, runuser and notify-send are shimmed onto PATH so every branch
|
||||
# can be reached from an ordinary account.
|
||||
@@ -14,7 +14,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SCRIPT="$ROOT/scripts/classroom-notify"
|
||||
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"; kill "${FAKE_SWAY_PID:-}" 2>/dev/null' EXIT
|
||||
trap 'rm -rf "$WORK"; kill "${FAKE_SESSION_PID:-}" 2>/dev/null' EXIT
|
||||
|
||||
passed=0
|
||||
failed=0
|
||||
@@ -67,12 +67,12 @@ SHIM
|
||||
cat > "$WORK/bin/pgrep" <<'SHIM'
|
||||
#!/bin/bash
|
||||
case "${!#}" in
|
||||
sway)
|
||||
[[ -n "${FAKE_SWAY_PID:-}" ]] || exit 1
|
||||
echo "$FAKE_SWAY_PID"
|
||||
kwin_wayland)
|
||||
[[ -n "${FAKE_SESSION_PID:-}" ]] || exit 1
|
||||
echo "$FAKE_SESSION_PID"
|
||||
;;
|
||||
mako)
|
||||
[[ "${FAKE_MAKO:-1}" == "1" ]] || exit 1
|
||||
plasmashell)
|
||||
[[ "${FAKE_SHELL:-1}" == "1" ]] || exit 1
|
||||
echo 4242
|
||||
;;
|
||||
*)
|
||||
@@ -120,27 +120,27 @@ run() {
|
||||
"$SCRIPT" "$@" 2>&1
|
||||
}
|
||||
|
||||
start_fake_sway() {
|
||||
start_fake_session() {
|
||||
env "$@" sleep 300 &
|
||||
FAKE_SWAY_PID=$!
|
||||
export FAKE_SWAY_PID
|
||||
FAKE_SESSION_PID=$!
|
||||
export FAKE_SESSION_PID
|
||||
# Give the exec time to land so /proc/<pid>/environ is populated.
|
||||
while [[ ! -r "/proc/$FAKE_SWAY_PID/environ" ]]; do sleep 0.05; done
|
||||
while [[ ! -r "/proc/$FAKE_SESSION_PID/environ" ]]; do sleep 0.05; done
|
||||
}
|
||||
|
||||
stop_fake_sway() {
|
||||
[[ -n "${FAKE_SWAY_PID:-}" ]] && kill "$FAKE_SWAY_PID" 2>/dev/null
|
||||
wait "${FAKE_SWAY_PID:-}" 2>/dev/null
|
||||
unset FAKE_SWAY_PID
|
||||
stop_fake_session() {
|
||||
[[ -n "${FAKE_SESSION_PID:-}" ]] && kill "$FAKE_SESSION_PID" 2>/dev/null
|
||||
wait "${FAKE_SESSION_PID:-}" 2>/dev/null
|
||||
unset FAKE_SESSION_PID
|
||||
}
|
||||
|
||||
unset WAYLAND_DISPLAY DBUS_SESSION_BUS_ADDRESS
|
||||
|
||||
# --------------------------------------------- 1. resolve from environ
|
||||
|
||||
echo "1. resolves the session from sway's environment"
|
||||
echo "1. resolves the session from the compositor's environment"
|
||||
|
||||
start_fake_sway \
|
||||
start_fake_session \
|
||||
XDG_RUNTIME_DIR="$RUNTIME" \
|
||||
DBUS_SESSION_BUS_ADDRESS="unix:path=$RUNTIME/bus" \
|
||||
WAYLAND_DISPLAY=wayland-7
|
||||
@@ -149,9 +149,9 @@ out="$(run)"
|
||||
check "reports delivery to the classroom session" "Sent normal notification to the classroom desktop session" "$out"
|
||||
|
||||
log="$(cat "$NOTIFY_LOG")"
|
||||
check "runtime dir taken from sway" "XDG_RUNTIME_DIR=$RUNTIME" "$log"
|
||||
check "bus address taken from sway" "DBUS_SESSION_BUS_ADDRESS=unix:path=$RUNTIME/bus" "$log"
|
||||
check "wayland display from sway" "WAYLAND_DISPLAY=wayland-7" "$log"
|
||||
check "runtime dir taken from the compositor" "XDG_RUNTIME_DIR=$RUNTIME" "$log"
|
||||
check "bus address taken from the compositor" "DBUS_SESSION_BUS_ADDRESS=unix:path=$RUNTIME/bus" "$log"
|
||||
check "wayland display from the compositor" "WAYLAND_DISPLAY=wayland-7" "$log"
|
||||
check "styling preserved" "--app-name=Ekklesia Classroom" "$log"
|
||||
check "default sample title" "DVD Ready" "$log"
|
||||
check "default sample body" "A disc is ready to play." "$log"
|
||||
@@ -161,7 +161,7 @@ check "default sample body" "A disc is ready to play."
|
||||
echo "2. --dry-run reports the resolved session and sends nothing"
|
||||
|
||||
out="$(run --dry-run)"
|
||||
check "shows the sway pid" "sway pid: $FAKE_SWAY_PID" "$out"
|
||||
check "shows the compositor pid" "compositor pid: $FAKE_SESSION_PID" "$out"
|
||||
check "shows the runtime dir" "XDG_RUNTIME_DIR: $RUNTIME" "$out"
|
||||
check "shows the wayland display" "WAYLAND_DISPLAY: wayland-7" "$out"
|
||||
check_absent "nothing was sent" "args:" "$(cat "$NOTIFY_LOG")"
|
||||
@@ -196,35 +196,35 @@ check "explains it needs root" "run this as root (sudo classroom-notify)" "$out"
|
||||
check_absent "nothing was sent" "args:" "$(cat "$NOTIFY_LOG")"
|
||||
FAKE_CURRENT_UID=4000
|
||||
|
||||
# ----------------------------------------- 6. mako must already be up
|
||||
# ------------------------------- 6. the shell must already be up
|
||||
|
||||
echo "6. refuses to let D-Bus start a second mako"
|
||||
echo "6. refuses to let D-Bus start a notification service"
|
||||
|
||||
export FAKE_MAKO=0
|
||||
export FAKE_SHELL=0
|
||||
out="$(run)"
|
||||
check "explains mako is not running" "mako is not running in the classroom session" "$out"
|
||||
check "explains the shell is not running" "the classroom shell is not running" "$out"
|
||||
check_absent "nothing was sent" "args:" "$(cat "$NOTIFY_LOG")"
|
||||
unset FAKE_MAKO
|
||||
unset FAKE_SHELL
|
||||
|
||||
stop_fake_sway
|
||||
stop_fake_session
|
||||
|
||||
# --------------------------------- 7. display derived from the lock fd
|
||||
|
||||
echo "7. derives the display from sway's wayland lock when environ has none"
|
||||
echo "7. derives the display from the compositor's wayland lock when environ has none"
|
||||
|
||||
touch "$RUNTIME/wayland-9.lock"
|
||||
env -u WAYLAND_DISPLAY \
|
||||
XDG_RUNTIME_DIR="$RUNTIME" \
|
||||
DBUS_SESSION_BUS_ADDRESS="unix:path=$RUNTIME/bus" \
|
||||
bash -c "exec 9< '$RUNTIME/wayland-9.lock'; sleep 300" &
|
||||
FAKE_SWAY_PID=$!
|
||||
export FAKE_SWAY_PID
|
||||
while [[ ! -r "/proc/$FAKE_SWAY_PID/fd/9" ]]; do sleep 0.05; done
|
||||
FAKE_SESSION_PID=$!
|
||||
export FAKE_SESSION_PID
|
||||
while [[ ! -r "/proc/$FAKE_SESSION_PID/fd/9" ]]; do sleep 0.05; done
|
||||
|
||||
out="$(run --dry-run)"
|
||||
check "display derived from the lock" "WAYLAND_DISPLAY: wayland-9" "$out"
|
||||
|
||||
stop_fake_sway
|
||||
stop_fake_session
|
||||
|
||||
# ------------------------------------------- 8. no session at all
|
||||
|
||||
@@ -236,9 +236,9 @@ check_absent "nothing was sent" "args:" "$(cat "$NOTIFY_LOG")"
|
||||
|
||||
# ------------------------------------- 9. falls back when environ is thin
|
||||
|
||||
echo "9. falls back to session defaults when sway's environment is unreadable"
|
||||
echo "9. falls back to session defaults when the compositor's environment is unreadable"
|
||||
|
||||
start_fake_sway XDG_RUNTIME_DIR="$RUNTIME"
|
||||
start_fake_session XDG_RUNTIME_DIR="$RUNTIME"
|
||||
|
||||
out="$(run --dry-run)"
|
||||
check "derives the bus from the runtime dir" \
|
||||
@@ -249,7 +249,7 @@ check "still delivers" "Sent normal notification to the classroom desktop sessio
|
||||
check "bus reached notify-send" "DBUS_SESSION_BUS_ADDRESS=unix:path=$RUNTIME/bus" \
|
||||
"$(cat "$NOTIFY_LOG")"
|
||||
|
||||
stop_fake_sway
|
||||
stop_fake_session
|
||||
|
||||
# ------------------------------------------ 10. missing bus is explained
|
||||
|
||||
@@ -257,19 +257,19 @@ echo "10. explains a missing session bus instead of hanging"
|
||||
|
||||
MISSING="$WORK/empty"
|
||||
mkdir -p "$MISSING"
|
||||
start_fake_sway XDG_RUNTIME_DIR="$MISSING"
|
||||
start_fake_session XDG_RUNTIME_DIR="$MISSING"
|
||||
|
||||
out="$(run)"
|
||||
check "names the missing socket" "bus socket $MISSING/bus does not exist" "$out"
|
||||
check_absent "nothing was sent" "args:" "$(cat "$NOTIFY_LOG")"
|
||||
|
||||
stop_fake_sway
|
||||
stop_fake_session
|
||||
|
||||
# ----------------------------- 11. already inside the classroom session
|
||||
|
||||
echo "11. sends directly when already inside the classroom session"
|
||||
|
||||
# No sway process to find: an in-session caller must not need one.
|
||||
# No compositor process to find: an in-session caller must not need one.
|
||||
export WAYLAND_DISPLAY=wayland-4
|
||||
export DBUS_SESSION_BUS_ADDRESS="unix:path=$RUNTIME/bus"
|
||||
export XDG_RUNTIME_DIR="$RUNTIME"
|
||||
|
||||
@@ -16,7 +16,10 @@ provision = (root / "scripts/classroom-provision").read_text()
|
||||
units = "\n".join(path.read_text() for path in (root / "systemd").rglob("*.service"))
|
||||
canary = (root / "update_canary.sh").read_text()
|
||||
validation_script = (root / "tools/validate-trixie-release.sh").read_text()
|
||||
sway = (root / "config/sway/config").read_text()
|
||||
layout = (root / "config/plasma/lookandfeel/contents/layouts/org.kde.plasma.desktop-layout.js").read_text()
|
||||
rules = (root / "config/plasma/xdg/kwinrulesrc").read_text()
|
||||
restrictions = (root / "config/plasma/xdg/kdeglobals").read_text()
|
||||
supervision = (root / "systemd/user/plasma-plasmashell.service.d/ekklesia-classroom.conf").read_text()
|
||||
build = (root / "tools/build-release.sh").read_text()
|
||||
policy = json.loads(
|
||||
(root / "config/chrome/policies/managed/ekklesia-classroom.json").read_text()
|
||||
@@ -67,14 +70,41 @@ check("printer provisioning is bounded", "--kill-after=5s 30s" in provision)
|
||||
check("Chrome uses the system print dialog", policy.get("DisablePrintPreview") is True)
|
||||
|
||||
print()
|
||||
print("4. secondary Chrome windows")
|
||||
check("stateful Chrome helper starts with Sway", "/usr/local/bin/classroom-chrome-windows" in sway)
|
||||
check("Chrome helper is packaged", '"$ROOT/scripts/classroom-chrome-windows"' in build)
|
||||
check("Chrome role module is packaged", '"$ROOT/lib/classroom_chrome.py"' in build)
|
||||
check("no broad Chrome floating rule", not any(
|
||||
"google-chrome" in line and "floating enable" in line
|
||||
for line in sway.splitlines()
|
||||
))
|
||||
print("4. secondary windows are the window manager's business")
|
||||
# Chrome gives its main window and every window a page opens the same
|
||||
# class, so a rule keyed on that class maximises the lesson's popup too.
|
||||
# It was measured doing exactly that. The browser is told to start
|
||||
# maximized instead, and no rule mentions it.
|
||||
check("no window rule claims the browser", "google-chrome" not in rules)
|
||||
check("the browser asks for its own first window", "--start-maximized" in
|
||||
(root / "scripts/classroom-browser").read_text())
|
||||
check("Wonder Ink asks for the same", "--start-maximized" in
|
||||
(root / "scripts/classroom-wonderink").read_text())
|
||||
check("the Chrome window arbiter is gone", not (root / "scripts/classroom-chrome-windows").exists())
|
||||
check("and is not packaged", "classroom-chrome-windows" not in build)
|
||||
|
||||
print()
|
||||
print("4b. the panel is never allowed to stay dead")
|
||||
# Plasma restarts a crashed panel three times a minute and then gives up.
|
||||
# Measured: dead on the third kill. These lines are what make it
|
||||
# unconditional, and losing them would lose the room its only way around.
|
||||
check("restart is unconditional", "Restart=always" in supervision)
|
||||
check("the burst limit is lifted", "StartLimitIntervalSec=0" in supervision)
|
||||
check("the drop-in is packaged", "plasma-plasmashell.service.d" in build)
|
||||
|
||||
print()
|
||||
print("4c. the teacher cannot reach a command line")
|
||||
for restriction in ("run_command=false", "shell_access=false",
|
||||
"action/lock_screen=false",
|
||||
"plasma/plasmashell/unlockedDesktop=false"):
|
||||
check(f"{restriction}", restriction in restrictions)
|
||||
check("the restrictions are immutable", "[KDE Action Restrictions][$i]" in restrictions)
|
||||
|
||||
print()
|
||||
print("4d. clicking a launcher never hides an application")
|
||||
check("minimise on click is off", 'minimizeActiveTaskOnClick", false' in layout)
|
||||
check("there is no minimise button", "ButtonsOnRight=FX" in
|
||||
(root / "config/plasma/xdg/kwinrc").read_text())
|
||||
|
||||
print()
|
||||
print("5. bounded Trixie validation")
|
||||
|
||||
Reference in New Issue
Block a user