#!/bin/bash # Which applications a device offers, and the bar that comes out of it. # # Two halves. The first is the decision — one setting per application, and # never the shape of the machine. The second actually RUNS the layout # script the package ships, in node, with Plasma's API stubbed, so the # assertions are about the real file rather than about a copy of its text # that could drift away from it. set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PYTHONPATH="$ROOT/lib" python3 - "$ROOT" <<'PY' import importlib.util import json import os import shutil import subprocess import sys import tempfile from importlib.machinery import SourceFileLoader from pathlib import Path root = Path(sys.argv[1]) loader = SourceFileLoader("layout", str(root / "scripts/classroom-desktop-layout")) spec = importlib.util.spec_from_loader("layout", loader) module = importlib.util.module_from_spec(spec) loader.exec_module(module) LAYOUT = root / "config/plasma/lookandfeel/contents/layouts/org.kde.plasma.desktop-layout.js" 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 BLOCK = """/* CLASSROOM-FEATURES-BEGIN */ var WONDERINK = true; var DVD = true; var PARENT_CALL = true; /* CLASSROOM-FEATURES-END */""" print("1. one line per application, rewritten in place") check("a feature can be switched off", "var WONDERINK = false;" in module.rewrite(BLOCK, "WONDERINK", False)) check("and back on again", "var WONDERINK = true;" in module.rewrite(module.rewrite(BLOCK, "WONDERINK", False), "WONDERINK", True)) check("switching one leaves the others alone", "var DVD = true;" in module.rewrite(BLOCK, "WONDERINK", False) and "var PARENT_CALL = true;" in module.rewrite(BLOCK, "WONDERINK", False)) check("rewriting twice changes nothing the second time", module.rewrite(module.rewrite(BLOCK, "DVD", False), "DVD", False) == module.rewrite(BLOCK, "DVD", False)) print("2. the shape of the machine never decides what it is for") # This is the architecture rule, and it is the one worth a test: a laptop # must be able to run children's church and a tower must be able to be a # Bible study. module.has_optical_drive = lambda: True laptop_with_ink = module.wanted({"DEVICE_TYPE": "laptop"}) check("a laptop still offers Wonder Ink unless told otherwise", laptop_with_ink["WONDERINK"] is True, str(laptop_with_ink)) check("a laptop still offers Parent Call unless told otherwise", laptop_with_ink["PARENT_CALL"] is True) desktop_no_ink = module.wanted( {"DEVICE_TYPE": "desktop", "WONDERINK_ENABLED": "false"}) check("a desktop can have Wonder Ink taken away", desktop_no_ink["WONDERINK"] is False) check("and keeps everything else", desktop_no_ink["DVD"] is True and desktop_no_ink["PARENT_CALL"] is True) check("an existing room that has never heard of these keeps them all", all(module.wanted({}).values()), str(module.wanted({}))) print("3. a drive can only ever take the player away") module.has_optical_drive = lambda: False check("no drive means no player, whatever the setting says", module.wanted({"DVD_ENABLED": "true"})["DVD"] is False) module.has_optical_drive = lambda: True check("a drive plus DVD_ENABLED=false is still no player", module.wanted({"DVD_ENABLED": "false"})["DVD"] is False) check("a drive plus DVD_ENABLED=true is the player", module.wanted({"DVD_ENABLED": "true"})["DVD"] is True) print("4. the script rewrites the layout the package actually ships") module.has_optical_drive = lambda: True def run(config): """Run the real script over a copy of the real layout.""" work = Path(tempfile.mkdtemp(prefix="classroom-layout-")) copy = work / "layout.js" shutil.copy(LAYOUT, copy) module.read_device_config = lambda: config module.main.__globals__["sys"].argv = ["classroom-desktop-layout", str(copy)] module.main() return copy.read_text(), work text, work = run({}) check("the shipped layout carries the feature block", module.MARKER in text) check("and an unconfigured machine keeps every application", "var WONDERINK = true;" in text and "var DVD = true;" in text and "var PARENT_CALL = true;" in text) print("5. the bar that comes out, evaluated in node") STUB = r""" const fs = require("fs"); const captured = {widgets: [], config: {}}; let current = null; function makeWidget(type) { const w = { type: type, currentConfigGroup: [], writeConfig: function (k, v) { captured.config[type + "." + k] = String(v); }, reloadConfig: function () {} }; captured.widgets.push(type); return w; } globalThis.desktops = () => [makeWidget("desktop")]; globalThis.Panel = function () { this.addWidget = makeWidget; return this; }; eval(fs.readFileSync(process.argv[2], "utf8")); console.log(JSON.stringify(captured)); """ def bar(config): text, work = run(config) stub = work / "stub.js" stub.write_text(STUB) result = subprocess.run( ["node", str(stub), str(work / "layout.js")], capture_output=True, text=True, timeout=60, ) if result.returncode != 0: return None, result.stderr return json.loads(result.stdout), "" if shutil.which("node") is None: print(" SKIP node is not installed; the layout was not evaluated") else: LAUNCH = "org.kde.plasma.icontasks.launchers" INK = "applications:ekklesia-wonderink.desktop" WEB = "applications:google-chrome.desktop" DISC = "applications:ekklesia-dvd.desktop" every, why = bar({}) check("a classroom gets Wonder Ink, the browser and the player, in order", every and every["config"].get(LAUNCH) == f"{INK},{WEB},{DISC}", (every or {}).get("config", {}).get(LAUNCH, why)) check("and Parent Call in the corner", every and "org.kde.plasma.quicklaunch" in every["widgets"]) no_ink, why = bar({"WONDERINK_ENABLED": "false"}) check("a Bible study desktop has the browser first and no gap", no_ink and no_ink["config"].get(LAUNCH) == f"{WEB},{DISC}", (no_ink or {}).get("config", {}).get(LAUNCH, why)) module.has_optical_drive = lambda: False bible, why = bar({"WONDERINK_ENABLED": "false", "PARENT_CALL_ENABLED": "false", "DEVICE_TYPE": "laptop"}) check("the laptop's bar is the browser alone", bible and bible["config"].get(LAUNCH) == WEB, (bible or {}).get("config", {}).get(LAUNCH, why)) check("with no Parent Call button on it", bible and "org.kde.plasma.quicklaunch" not in bible["widgets"], str((bible or {}).get("widgets"))) check("and the status corner is untouched", bible and "org.kde.plasma.volume" in bible["widgets"] and "org.kde.plasma.digitalclock" in bible["widgets"] and "org.kde.plasma.networkmanagement" in bible["widgets"]) module.has_optical_drive = lambda: True ink_laptop, why = bar({"DEVICE_TYPE": "laptop"}) check("a laptop taking children's church gets the same bar as a tower", ink_laptop and ink_laptop["config"].get(LAUNCH) == f"{INK},{WEB},{DISC}", (ink_laptop or {}).get("config", {}).get(LAUNCH, why)) print("6. the bar cannot be rearranged by dragging") check("sorting is off, which is what refuses a reordering drop", every and every["config"].get( "org.kde.plasma.icontasks.sortingStrategy") == "0", str((every or {}).get("config", {}).get( "org.kde.plasma.icontasks.sortingStrategy"))) check("and launchers stay ahead of running windows", every and every["config"].get( "org.kde.plasma.icontasks.separateLaunchers") == "true") print("7. a disabled application does not start either") # Taking the launcher off the bar is half of it. The command itself has # to refuse, so a device that does not offer an application cannot be # talked into running it by a stale desktop entry or a technician's # muscle memory. for script, setting in (("classroom-wonderink", "WONDERINK_ENABLED"), ("classroom-parent-call", "PARENT_CALL_ENABLED")): text = (root / "scripts" / script).read_text() check(f"{script} refuses when {setting} is false", setting in text and "not starting" in text) check(f"{script} defaults to starting, so existing rooms are unaffected", 'default=True' in text or 'feature(' in text) print() print(f"{passed} passed, {failed} failed") raise SystemExit(1 if failed else 0) PY