Files
Classroom_Desktop/tests/test-classroom-desktop-layout.sh
T

318 lines
13 KiB
Bash
Executable File

#!/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)
unset = module.wanted({})
check("an existing room that has never heard of these keeps all three",
unset["WONDERINK"] and unset["DVD"] and unset["PARENT_CALL"],
str(unset))
# The one that must NOT follow that rule. Every existing classroom has
# Wonder Ink, a drive and Parent Call, so an upgrade has to leave all
# three on. No existing classroom has Presentations, so an upgrade has to
# leave it off — a new button on a bar nobody asked for is a support call.
check("but Presentations is off until a device asks for it",
unset["PRESENTATIONS"] is False, str(unset))
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: {}, launchers: []};
let current = null;
function makeWidget(type) {
const w = {
type: type,
currentConfigGroup: [],
writeConfig: function (k, v) {
captured.config[type + "." + k] = String(v);
// Quick Launch widgets share a config key, so the last one
// written would otherwise be the only one anybody could see.
if (k === "launcherUrls") { captured.launchers.push(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))
CALL = "file:///usr/share/applications/ekklesia-parent-call.desktop"
HELP = "file:///usr/share/applications/ekklesia-manual.desktop"
check("and Parent Call in the corner",
every and CALL in every["launchers"],
str((every or {}).get("launchers")))
check("and the unfinished Help launcher is hidden",
every and HELP not in every["launchers"],
str((every or {}).get("launchers")))
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 CALL not in bible["launchers"],
str((bible or {}).get("launchers")))
# The source and launcher remain installed, but the unfinished digital
# Help entry point is deliberately absent from the teacher-facing bar.
check("and unfinished Help stays hidden",
bible and HELP not in bible["launchers"],
str((bible or {}).get("launchers")))
SLIDES = "applications:ekklesia-presentations.desktop"
# THE DOCK ORDER IS THE THING NOT TO BREAK.
#
# Every existing room has Wonder Ink, the browser and the player in
# that order, and teachers have been pressing them in that order for
# months. Presentations is appended, so all three keep their place on
# every machine that upgrades into this release.
module.has_optical_drive = lambda: True
slides, why = bar({"PRESENTATIONS_ENABLED": "true"})
check("Presentations goes on the END of the row, after the player",
slides and slides["config"].get(LAUNCH)
== f"{INK},{WEB},{DISC},{SLIDES}",
(slides or {}).get("config", {}).get(LAUNCH, why))
without, why = bar({"PRESENTATIONS_ENABLED": "false"})
check("and is absent entirely when the device does not offer it",
without and SLIDES not in without["config"].get(LAUNCH, ""),
(without or {}).get("config", {}).get(LAUNCH, why))
check("which leaves the other three exactly where they were",
without and without["config"].get(LAUNCH) == f"{INK},{WEB},{DISC}",
(without or {}).get("config", {}).get(LAUNCH, why))
# The prayer-room laptop: the browser and Presentations, nothing else.
module.has_optical_drive = lambda: False
study, why = bar({"PRESENTATIONS_ENABLED": "true",
"WONDERINK_ENABLED": "false",
"PARENT_CALL_ENABLED": "false",
"DEVICE_TYPE": "laptop"})
check("the Bible study laptop gets the browser and Presentations",
study and study["config"].get(LAUNCH) == f"{WEB},{SLIDES}",
(study or {}).get("config", {}).get(LAUNCH, why))
check("and still no Parent Call on it",
study and CALL not in study["launchers"],
str((study or {}).get("launchers")))
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 order survives an application opening")
# 1, "manually", is the ONLY value at which the Icons-Only Task
# Manager keeps a running application in its pinned position:
# main.qml gates launchInPlace on sortingStrategy === 1, and for an
# icons-only bar the launcher is always hidden once its application
# starts. 0.2.4 set this to 0 and the browser jumped behind the disc
# player the moment it opened.
#
# This assertion is the mechanism. The behaviour itself is checked
# against real windows by tools/lab/order-check, because a
# configuration test is exactly what 0.2.4 passed.
check("sorting is manual, which is what keeps a launcher in place",
every and every["config"].get(
"org.kde.plasma.icontasks.sortingStrategy") == "1",
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"),
("classroom-presentations", "PRESENTATIONS_ENABLED"),
("classroom-presentation-import",
"PRESENTATIONS_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} asks the shared helper rather than reading the file",
"feature(" in text and "read_device_config()" in text)
# And each of them defaults the way its own room needs, which is not the
# same answer for all four. Asserting one rule for all of them would pass
# for the wrong reason on the one that differs.
from classroom_config import feature as _feature # noqa: E402
for setting, default, why in (
("WONDERINK_ENABLED", True, "every existing classroom has it"),
("DVD_ENABLED", True, "every existing classroom has it"),
("PARENT_CALL_ENABLED", True, "every existing classroom has it"),
("PRESENTATIONS_ENABLED", False, "no existing classroom has it")):
check(f"{setting} defaults to {default} — {why}",
_feature({}, setting) is default)
print()
print(f"{passed} passed, {failed} failed")
raise SystemExit(1 if failed else 0)
PY