Teachers prepare a lesson across several tabs — the lesson, its video,
the activity — and app mode is the one thing that cannot give them that.
Chrome cannot be asked for "tabs but no omnibox". App mode has no tabs; a
normal window has an address bar and no policy, flag or extension removes
it; tabbed web apps need a manifest on a site that is not ours and push
off-origin links out into a normal browser window, which is the whole of
what this design exists to prevent. So the tab strip is ours, and the
engine is Chromium either way.
QtWebEngine, from Debian, measured before it was chosen rather than
after. The usual and correct objection is codecs: on Debian 13 it reports
H.264, AAC, MP3, VP8 and VP9 all playable, MediaSource supports avc1,
mp4a and vp9, and a 720p H.264 file decoded and played to the end in the
lab. app.wonderink.org loads, and its sign-in is an ordinary email and
password form rather than a Google flow that would mind an embedded
browser. The user agent has the QtWebEngine token stripped so a site
gating on it sees the Chromium this actually is, and Widevine is handed
over from Chrome's own bundle when the machine has Chrome, which a
classroom always does.
[ Lesson ] [ Video ] [ Activity ]
The strip appears only above one tab, so an ordinary lesson looks exactly
like the application it replaces. A tab can only come from the lesson —
target=_blank, window.open, a middle click, a Ctrl+click — and there is
no address bar, no search and no button that makes a blank tab. No
allowlist of domains either, on purpose: a list would have to be right
about every video service a lesson might ever embed.
Three bugs the lab caught and this fixes. A lesson calling window.print()
did nothing at all, which is worse than no print button because a teacher
concludes the printer is broken; printRequested is now bound to the page
that asked. Leaving fullscreen left the window 200 pixels wide in the
corner — a window that has only ever been maximised has no ordinary
geometry to return to — so the fullscreen bit is cleared rather than
maximised being re-requested. And the Present view video failure written
up in docs/wonder-ink.md is acted on at last: that investigation named
the autoplay gesture chain as the likeliest cause and declined to fix it,
because in Chrome the only lever governed the ordinary browser as well.
Here it does not — this profile is Wonder Ink and nothing else — so a
lesson may start its own video and no other page in the room may.
Reproduced both ways in the lab with an unmuted video started by a script
with no gesture: NotAllowedError at Chrome's default, playing as shipped.
That is not proof Present view is fixed; the lab has no Wonder Ink
account, and candidates 2 and 3 in that document are still open.
Measured on a rebooted machine, from the package: tabs open from a link
and from window.open, switch, and close; closing one leaves the others;
closing the last closes Wonder Ink; H.264 video plays and fills the
display in fullscreen and comes back maximised with its tabs; the print
dialog offers the room's own Elmer queue; a download lands in ~/Downloads;
back and forward work from a right-click menu that is built rather than
filtered; pressing the panel button twice raises one window rather than
starting a second on the same profile; KWin reports the window as
ekklesia-wonderink, which is one panel button and not two.
Separate from the browser as before, in its own profile, with cookies
that live only as long as the window — what the Chrome policy did for
wonderink.org, and the reason closing Wonder Ink ends a teacher's
session. Chrome's old profile directory is left untouched beside it, and
the browser's own policy is not edited by any of this.
Wonder Ink is now optional per device, through WONDERINK_ENABLED in
device.conf, because one future device is a laptop that will not have it.
It rides the mechanism the disc player already uses: one marked line in
the packaged layout, edited by classroom-desktop-layout. False takes the
launcher off the panel and makes the command refuse to start; the
browser, Parent Call and the disc player are untouched and the panel
stays balanced. Absent means yes, so every existing room is unchanged.
One scale, still. Chrome has to be handed UI_SCALE because it was
measured not to follow the compositor; Qt does follow it — devicePixelRatio
2.0 at UI_SCALE=2.0, measured — so Wonder Ink is deliberately not told,
or the room would get it twice.
Co-Authored-By: Claude Opus 5 <[email protected]>
397 lines
20 KiB
Bash
Executable File
397 lines
20 KiB
Bash
Executable File
#!/bin/bash
|
|
set -uo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
python3 - "$ROOT" <<'PY'
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
root = Path(sys.argv[1])
|
|
control = (root / "packaging/debian/control.template").read_text()
|
|
postinst = (root / "packaging/debian/postinst").read_text()
|
|
migrate = (root / "scripts/classroom-migrate").read_text()
|
|
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()
|
|
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()
|
|
chrome_policy = json.loads(
|
|
(root / "config/chrome/policies/managed/ekklesia-classroom.json").read_text()
|
|
)
|
|
|
|
passed = failed = 0
|
|
|
|
|
|
def check(label, condition):
|
|
global passed, failed
|
|
if condition:
|
|
print(f" ok {label}")
|
|
passed += 1
|
|
else:
|
|
print(f" FAIL {label}")
|
|
failed += 1
|
|
|
|
|
|
depends = re.search(r"^Depends: (.*)$", control, re.MULTILINE).group(1).split(", ")
|
|
|
|
print("1. Trixie package contract")
|
|
for package in (
|
|
"network-manager",
|
|
"cups",
|
|
"cups-client",
|
|
"cups-filters",
|
|
"foomatic-db-compressed-ppds",
|
|
):
|
|
check(f"depends on {package}", package in depends)
|
|
|
|
print()
|
|
print("2. networking service behavior")
|
|
check("NetworkManager is explicitly enabled", "systemctl enable NetworkManager.service || true" in postinst)
|
|
check("NetworkManager start is best effort", "systemctl start NetworkManager.service || true" in postinst)
|
|
check("online waiter is disabled", "systemctl disable NetworkManager-wait-online.service || true" in postinst)
|
|
check("no classroom NetworkManager config overrides ifupdown", not (root / "config/network").exists())
|
|
|
|
print()
|
|
print("3. printing service behavior")
|
|
check("CUPS service and socket are enabled", "systemctl enable cups.socket cups.service || true" in postinst)
|
|
check("CUPS start is best effort", "systemctl start cups.socket || true" in postinst)
|
|
check("automatic remote queue browsing is disabled", "systemctl disable --now cups-browsed.service || true" in postinst)
|
|
check("Avahi service and socket are disabled", "systemctl disable --now avahi-daemon.socket avahi-daemon.service || true" in postinst)
|
|
check("Avahi service and socket are masked", "systemctl mask avahi-daemon.socket avahi-daemon.service || true" in postinst)
|
|
check("printer setup is not a blocking boot dependency", "classroom-printer-setup" not in units)
|
|
check("printer setup runs automatically in provisioning", "/usr/local/bin/classroom-printer-setup" in provision)
|
|
check("printer provisioning is bounded", "--kill-after=5s 30s" in provision)
|
|
check("Chrome uses the system print dialog",
|
|
chrome_policy.get("DisablePrintPreview") is True)
|
|
|
|
print()
|
|
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())
|
|
# Wonder Ink is no longer a Chrome window, so it asks in its own words.
|
|
check("Wonder Ink asks for the same", "showMaximized()" 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("4b2. the session settings do not fight another package")
|
|
# plasma-workspace owns /etc/xdg/kdeglobals and friends. dpkg refused the
|
|
# package when the classroom tried to own them too, so the room's copies
|
|
# live in a directory of their own, in front of /etc/xdg.
|
|
check("settings install to the classroom's own directory",
|
|
"$STAGE/etc/ekklesia-classroom/xdg/" in build)
|
|
# One file of ours does live under /etc/xdg, and it is not a settings
|
|
# file: it is the session environment drop-in that puts the directory
|
|
# above into the cascade at all. A drop-in directory is built to be
|
|
# added to; a name plasma-workspace already owns is not.
|
|
xdg_paths = sorted(re.findall(r'\$STAGE(/etc/xdg/[^"]+)', build))
|
|
check("what the classroom puts under /etc/xdg is two drop-ins",
|
|
xdg_paths == [
|
|
"/etc/xdg/autostart/ekklesia-classroom-scale.desktop",
|
|
"/etc/xdg/plasma-workspace/env/zz-ekklesia-classroom.sh",
|
|
])
|
|
check("and no settings file takes a name plasma-workspace owns",
|
|
not any(path.rsplit("/", 1)[0] == "/etc/xdg" for path in xdg_paths))
|
|
check("and that directory is searched first",
|
|
"XDG_CONFIG_DIRS=/etc/ekklesia-classroom/xdg:/etc/xdg" in
|
|
(root / "config/environment/90-ekklesia-classroom.conf").read_text())
|
|
check("the environment file is packaged",
|
|
"/etc/environment.d/90-ekklesia-classroom.conf" in build)
|
|
|
|
print()
|
|
print("4b3. every icon the panel names is in the package")
|
|
import configparser
|
|
for entry in sorted((root / "config/plasma/applications").glob("*.desktop")):
|
|
parser = configparser.ConfigParser(interpolation=None, strict=False)
|
|
parser.read(entry)
|
|
icon = parser["Desktop Entry"].get("icon", "")
|
|
if not icon.startswith("ekklesia-"):
|
|
continue
|
|
check(f"{icon} is installed by the package",
|
|
f'"$ROOT/config/icons/${{icon}}.svg"' in build or icon in build)
|
|
check(f"{icon}.svg exists", (root / f"config/icons/{icon}.svg").is_file())
|
|
|
|
print()
|
|
print("4b4. the power button does nothing, at both layers")
|
|
logind = (root / "config/systemd/logind-ekklesia-classroom.conf").read_text()
|
|
power = (root / "config/plasma/xdg/powermanagementprofilesrc").read_text()
|
|
# PowerDevil holds logind's handle-power-key inhibitor while a session is
|
|
# running, so one layer without the other is a button that still works.
|
|
for key in ("HandlePowerKey=ignore", "HandleSuspendKey=ignore",
|
|
"HandleHibernateKey=ignore", "HandleLidSwitch=ignore"):
|
|
check(f"logind: {key}", key in logind)
|
|
check("PowerDevil: no action on the power button",
|
|
power.count("powerButtonAction=0") == 3)
|
|
check("PowerDevil: no action on a lid either",
|
|
power.count("lidAction=0") == 3)
|
|
# Six: the three button groups, and the three profiles around them.
|
|
# PowerDevil loads an idle action when it finds a group for it, so a
|
|
# writable profile is one a leftover user file can add a suspend to.
|
|
check("and the session cannot put an action back",
|
|
power.count("[$i]") == 6)
|
|
check("no idle action in any profile",
|
|
not any(f"[{profile}][{action}]" in power
|
|
for profile in ("AC", "Battery", "LowBattery")
|
|
for action in ("SuspendSession", "DimDisplay", "DPMSControl")))
|
|
check("the logind drop-in is packaged",
|
|
"/etc/systemd/logind.conf.d/ekklesia-classroom.conf" in build)
|
|
check("the PowerDevil profile is packaged",
|
|
"powermanagementprofilesrc" in build)
|
|
check("the long hold is left alone on purpose",
|
|
"firmware" in logind)
|
|
|
|
print()
|
|
print("4b5. the maintenance terminal is hidden and reachable")
|
|
admin = (root / "config/plasma/applications/ekklesia-admin-terminal.desktop").read_text()
|
|
konsole = (root / "config/plasma/applications-local/org.kde.konsole.desktop").read_text()
|
|
wrapper = (root / "scripts/classroom-admin-terminal").read_text()
|
|
check("the chord is the obscure one",
|
|
"X-KDE-Shortcuts=Meta+Ctrl+Alt+Shift+K" in admin)
|
|
check("its entry is in no application list", "NoDisplay=true" in admin)
|
|
check("Konsole's own entry is shadowed and hidden",
|
|
"NoDisplay=true" in konsole)
|
|
check("the shadow is in /usr/local/share, not on top of Konsole's file",
|
|
"$STAGE/usr/local/share/applications/" in build)
|
|
check("the terminal is launched outside the classroom's restrictions",
|
|
"XDG_CONFIG_DIRS=/etc/xdg" in wrapper)
|
|
check("and grants no privilege of its own",
|
|
"sudo" not in wrapper.replace("no sudo rule", ""))
|
|
check("the wrapper is packaged", "classroom-admin-terminal" in build)
|
|
check("Konsole is a dependency, so the route exists on every machine",
|
|
"konsole" in (root / "packaging/debian/control.template").read_text())
|
|
check("no terminal is pinned to the panel",
|
|
"konsole" not in (root / "config/plasma/lookandfeel/contents/layouts"
|
|
/ "org.kde.plasma.desktop-layout.js").read_text().lower())
|
|
|
|
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")
|
|
check("container has an outer deadline", "720s" in validation_script)
|
|
check("package installs have deadlines", "candidate-install 360" in validation_script)
|
|
check("container service startup is suppressed", "/usr/sbin/policy-rc.d" in validation_script and "exit 101" in validation_script)
|
|
|
|
print()
|
|
print("6. canary release gate")
|
|
validation = canary.index('"$ROOT/tools/validate-trixie-release.sh"')
|
|
package_copy = canary.index('cp "$package_path"')
|
|
first_rsync = canary.index("rsync -av")
|
|
check("Trixie validation runs before the repository copy", validation < package_copy)
|
|
check("Trixie validation runs before any upload", validation < first_rsync)
|
|
# The gate checks convergence, not just that the package installs: an
|
|
# upgraded room ends on the v2 channel with every setting this release
|
|
# has, exactly once, and the command a technician runs is safe to run on
|
|
# a machine that is already converged.
|
|
check("the gate checks a deliberate channel survives",
|
|
"'^CHANNEL=canary$'" in validation_script)
|
|
check("and that the experiment's channel comes home",
|
|
"'^CHANNEL=stable$'" in validation_script)
|
|
check("and that every new setting arrived exactly once",
|
|
"for setting in LIVE_ENABLED AUDIO_OUTPUT UI_SCALE" in validation_script)
|
|
check("and that running the migration by hand is safe",
|
|
"classroom-migrate >/tmp/migrate-by-hand.log" in validation_script)
|
|
check("and that it never prints the broker password",
|
|
"! grep -q 'preserve-me' /tmp/migrate-by-hand.log" in validation_script)
|
|
|
|
print()
|
|
print("7. one scale, applied everywhere")
|
|
autostart = (root / "config/plasma/autostart/ekklesia-classroom-scale.desktop").read_text()
|
|
policy = (root / "config/polkit/org.ekklesia.classroom.display.policy").read_text()
|
|
settings_entry = (root / "config/plasma/applications/ekklesia-display-settings.desktop").read_text()
|
|
browser = (root / "scripts/classroom-browser").read_text()
|
|
wonderink = (root / "scripts/classroom-wonderink").read_text()
|
|
config_module = (root / "lib/classroom_config.py").read_text()
|
|
# UI_SCALE was packaged, clamped and documented, and nothing ever ran the
|
|
# command that applies it. A 4K television showed a 1080p interface at
|
|
# half size and every part of this section is a piece of that.
|
|
check("the scale is applied at every login",
|
|
"Exec=/usr/local/bin/classroom-scale" in autostart)
|
|
check("by the session, which is where kscreen-doctor can be asked",
|
|
"OnlyShowIn=KDE" in autostart)
|
|
check("and the package installs it",
|
|
"/etc/xdg/autostart/ekklesia-classroom-scale.desktop" in build)
|
|
check("changing it needs an administrator password",
|
|
"auth_admin" in policy and "classroom-set-scale" in policy)
|
|
check("and there is no way into it from the room",
|
|
"NoDisplay=true" in settings_entry
|
|
and "X-KDE-Shortcuts=Meta+Ctrl+Alt+Shift+D" in settings_entry)
|
|
check("2.0 is inside the range a room may ask for",
|
|
"UI_SCALE_MAX = 3.0" in config_module)
|
|
check("the browser is told the same number",
|
|
'--force-device-scale-factor="$SCALE"' in browser)
|
|
check("and asks the one command for it",
|
|
"classroom-ui-scale" in browser)
|
|
# Wonder Ink is deliberately NOT told. Chrome on Wayland was measured not
|
|
# to follow the compositor's output scale, which is why the browser has to
|
|
# be handed the number; Qt does follow it. Measured in the lab with
|
|
# UI_SCALE=2.0: kscreen-doctor reports the output at scale 2, and a Qt
|
|
# client on that session reports devicePixelRatio 2.0 without being told
|
|
# anything. Forcing it as well would apply the scale twice.
|
|
check("Wonder Ink is not told twice",
|
|
"force-device-scale-factor" not in wonderink
|
|
and "QT_SCALE_FACTOR" not in wonderink)
|
|
check("the clock is sized by the bar, not by a number",
|
|
'autoFontAndSize", true' in layout and "fontSize" not in layout)
|
|
|
|
print()
|
|
print("7b. the disc player is decided before a session starts")
|
|
layout_unit = (root / "systemd/layout/ekklesia-classroom-layout.service").read_text()
|
|
udev = (root / "config/udev/60-ekklesia-classroom-optical.rules").read_text()
|
|
reset = (root / "scripts/classroom-desktop-reset").read_text()
|
|
check("before the display manager", "Before=display-manager.service" in layout_unit)
|
|
check("started at every boot", "WantedBy=graphical.target" in layout_unit)
|
|
check("enabled by the package", "enable ekklesia-classroom-layout.service" in postinst)
|
|
check("a drive plugged in later is noticed", 'ENV{ID_CDROM}=="1"' in udev)
|
|
check("and the rules are reloaded on install", "udevadm control --reload-rules" in postinst)
|
|
check("the login says so when the layout and the hardware disagree",
|
|
"the disc player=" in reset)
|
|
|
|
print()
|
|
print("7c. the room's own popups")
|
|
style = (root / "config/plasma/desktoptheme/dialogs/background.svg").read_text()
|
|
plasmarc = (root / "config/plasma/xdg/plasmarc").read_text()
|
|
notify = (root / "config/plasma/xdg/plasmanotifyrc").read_text()
|
|
policy_notifications = chrome_policy.get("DefaultNotificationsSetting")
|
|
# A Plasma style is a set of pictures with a per-file fallback to Breeze.
|
|
# Replacing the notification plasmoid's QML was the other route and a QML
|
|
# error there is a room with no notifications at all.
|
|
for piece in ("topleft", "top", "topright", "left", "center", "right",
|
|
"bottomleft", "bottom", "bottomright"):
|
|
check(f"the card has a {piece}", f'id="{piece}"' in style)
|
|
check("the style is chosen, immutably", "name[$i]=ekklesia" in plasmarc)
|
|
check("and the package installs it", "desktoptheme/ekklesia" in build)
|
|
check("the colour still comes from the session's scheme",
|
|
"ColorScheme-Background" in style)
|
|
check("notifications stay long enough to read across a room",
|
|
"PopupTimeout=8000" in notify)
|
|
# The volume display is the one thing the classroom's shell package
|
|
# overrides. Plasma 6.3 loads the on-screen display from the shell
|
|
# package and from nowhere else — the look and feel is not consulted,
|
|
# which was measured — so this is the supported way to replace it. The
|
|
# fallback declaration is what keeps the package this small: without it a
|
|
# shell package loses the desktop's own defaults, and the wallpaper with
|
|
# them, which was also measured.
|
|
# The volume display is Plasma's own. Replacing it needs a shell package
|
|
# — 6.3 reads the on-screen display from there and nowhere else — and a
|
|
# shell package with one selected builds no panel at all. Measured twice,
|
|
# and the panel is rebuilt from the layout at every login on purpose, so
|
|
# the room would have come up empty. The scale makes Plasma's own display
|
|
# the right size on a television, which is what was actually wanted.
|
|
check("no shell package of our own", "plasma/shells" not in build)
|
|
check("and nothing selects one", "ShellPackage" not in build)
|
|
check("the volume chime is off", 'volumeFeedback", false' in layout)
|
|
check("and off in the setting Plasma 6 actually reads",
|
|
"AudioFeedback[$i]=false"
|
|
in (root / "config/plasma/xdg/plasmaparc").read_text())
|
|
check("but the display itself is on", 'volumeOsd", true' in layout)
|
|
check("web pages cannot put a notification on the television",
|
|
policy_notifications == 2)
|
|
check("and cannot ask to",
|
|
chrome_policy.get("NotificationsAllowedForUrls") == []
|
|
and chrome_policy.get("NotificationsBlockedForUrls") == ["*"])
|
|
|
|
print()
|
|
print("7c-i. the room does not introduce itself")
|
|
# A teacher does not need the name of the software. Every visible name is
|
|
# the thing it does — Parent Call, DVD, Browser, Classroom — and the
|
|
# product name stays where it belongs: package names, unit names, paths
|
|
# and topics, none of which anybody in the room reads.
|
|
visible = {}
|
|
|
|
for entry in sorted((root / "config/plasma").rglob("*.desktop")):
|
|
for line in entry.read_text().splitlines():
|
|
if line.split("=", 1)[0] in ("Name", "GenericName", "Comment"):
|
|
visible[f"{entry.name}:{line}"] = line
|
|
|
|
for script in sorted((root / "scripts").iterdir()):
|
|
if not script.is_file():
|
|
continue
|
|
for line in script.read_text().splitlines():
|
|
if "--app-name" in line:
|
|
visible[f"{script.name}:{line.strip()}"] = line
|
|
|
|
check("there are visible names to check", len(visible) > 10)
|
|
|
|
offenders = [where for where, line in visible.items() if "Ekklesia" in line]
|
|
|
|
if offenders:
|
|
for where in offenders:
|
|
print(f" {where}")
|
|
|
|
check(f"none of them says the product name ({len(visible)} checked)",
|
|
not offenders)
|
|
|
|
check("the notification card is from the room",
|
|
'--app-name="Classroom"'
|
|
in (root / "scripts/classroom-notify").read_text())
|
|
|
|
print()
|
|
print("7c-ii. the v2 channel is its own")
|
|
v2 = (root / "update_classroom_v2.sh").read_text()
|
|
# A publish script that can reach another channel's manifest is one
|
|
# mistake away from putting the Plasma build on a sway room.
|
|
code = "\n".join(line for line in v2.splitlines()
|
|
if not line.lstrip().startswith("#"))
|
|
check("it publishes to classroom-v2", "CHANNEL=classroom-v2" in code)
|
|
check("and names no other channel anywhere it acts",
|
|
"stable" not in code and "canary" not in code)
|
|
check("the same Trixie gate runs first",
|
|
code.index("validate-trixie-release.sh") < code.index("rsync"))
|
|
check("and the manifest goes up last",
|
|
code.rindex("manifest.json") > code.index("packages/"))
|
|
|
|
print()
|
|
print("7d. the broker every room shares")
|
|
mqtt_unit = (root / "systemd/mqtt/ekklesia-classroom-mqtt.service").read_text()
|
|
parent_call = (root / "lib/classroom_parent_call.py").read_text()
|
|
check("the link is restarted whatever it exits with",
|
|
"Restart=always" in mqtt_unit)
|
|
check("it is enabled by the package",
|
|
"enable ekklesia-classroom-mqtt.service" in postinst)
|
|
check("a fresh room points at the church broker",
|
|
"MQTT_HOST=10.40.0.2" in config_module)
|
|
check("an upgraded one is brought to the same place",
|
|
'BROKER_HOST = "10.40.0.2"' in migrate)
|
|
check("Parent Call tells a missing broker from an unreachable one",
|
|
"has no church network set up" in parent_call
|
|
and "Cannot reach the church network" in parent_call)
|
|
|
|
print()
|
|
print(f"passed: {passed} failed: {failed}")
|
|
raise SystemExit(1 if failed else 0)
|
|
PY
|