0.1.50 was taken to the church and the things a container cannot answer were answered on the hardware: boot, autologin, the panel after a reboot, panel and browser scaling, Wonder Ink, Parent Call opening in the corner and reaching the sanctuary with a typed name, readable and silent notifications, HDMI audio and the volume keys. The promotion is a fast-forward. The experiment was cut from the published stable source and never diverged from it, so master reaches this work without a merge and there is one history. One thing changed for the release. The channel rules turn around: classroom-v2 and hyprland are the obsolete channels now and a room on either comes home to stable, while canary — where the next development release goes — and any channel somebody named by hand stay where they were put. A fresh installation has always defaulted to stable. Nothing else moved. This is the 0.1.50 that was verified, with a version number and a channel rule. Co-Authored-By: Claude Opus 5 <[email protected]>
229 lines
7.3 KiB
Bash
Executable File
229 lines
7.3 KiB
Bash
Executable File
#!/bin/bash
|
|
set -uo pipefail
|
|
|
|
# What an upgrade does to a room's own settings file.
|
|
#
|
|
# device.conf is the one file in the classroom that belongs to the church
|
|
# rather than to the package: the room's name, and the broker password.
|
|
# An upgrade edits it, so an upgrade can also ruin it. These are the cases
|
|
# that matter, run against the real code — the migration's own Python is
|
|
# lifted out of the script rather than copied here, so a change to one
|
|
# cannot pass by disagreeing with the other.
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
python3 - "$ROOT" <<'PY'
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
root = Path(sys.argv[1])
|
|
sys.path.insert(0, str(root / "lib"))
|
|
|
|
from classroom_config import ( # noqa: E402
|
|
DEVICE_CONFIG_SECTIONS,
|
|
ensure_device_config,
|
|
)
|
|
|
|
script = (root / "scripts/classroom-migrate").read_text()
|
|
|
|
block = re.search(r'python3 - "\$DEVICE_CONFIG" <<\'PY\'\n(.*?)\nPY\n',
|
|
script, re.S)
|
|
|
|
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
|
|
|
|
|
|
check("the migration's device.conf code was found", block is not None)
|
|
|
|
if block is None:
|
|
sys.exit(1)
|
|
|
|
migration = block.group(1)
|
|
|
|
|
|
def migrate(text, shared=False):
|
|
"""Run the real migration over a device.conf and return it.
|
|
|
|
With shared=True, the shared helper runs afterwards as well, which is
|
|
the whole sequence classroom-migrate performs.
|
|
"""
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
source = Path(directory) / "migrate.py"
|
|
source.write_text(migration)
|
|
config = Path(directory) / "device.conf"
|
|
config.write_text(text)
|
|
|
|
result = subprocess.run(
|
|
[sys.executable, str(source), str(config)],
|
|
capture_output=True, text=True, check=False,
|
|
)
|
|
|
|
if shared:
|
|
ensure_device_config(config)
|
|
|
|
return config.read_text(), result.stdout + result.stderr
|
|
|
|
|
|
print("1. a room from the Hyprland experiment")
|
|
|
|
BEFORE = """# Ekklesia Classroom device configuration
|
|
|
|
UPDATE_URL=https://updates.riverofglorymn.org
|
|
CHANNEL=hyprland
|
|
LIVE_ENABLED=false
|
|
AUDIO_OUTPUT=auto
|
|
UI_SCALE=2.0
|
|
ROOM=little-waders
|
|
MQTT_ENABLED=false
|
|
MQTT_HOST=
|
|
MQTT_PORT=1883
|
|
MQTT_USERNAME=classroom
|
|
MQTT_PASSWORD=hunter2-not-a-real-one
|
|
COMPOSITOR=hyprland
|
|
HYPR_APPEARANCE=visual
|
|
"""
|
|
|
|
after, output = migrate(BEFORE)
|
|
|
|
check("the broker is switched on", "MQTT_ENABLED=true" in after)
|
|
check("and given the church address", "MQTT_HOST=10.40.0.2" in after)
|
|
check("the port is left as it was", "MQTT_PORT=1883" in after)
|
|
check("the room keeps its name", "ROOM=little-waders" in after)
|
|
check("the username is untouched", "MQTT_USERNAME=classroom" in after)
|
|
check("the password is untouched",
|
|
"MQTT_PASSWORD=hunter2-not-a-real-one" in after)
|
|
check("and is never printed", "hunter2" not in output)
|
|
check("the compositor setting is gone", "COMPOSITOR" not in after)
|
|
check("and the appearance setting with it", "HYPR_APPEARANCE" not in after)
|
|
check("the channel comes home to stable", "CHANNEL=stable" in after)
|
|
check("the room's scale is preserved", "UI_SCALE=2.0" in after)
|
|
check("and everything else is left alone",
|
|
"UPDATE_URL=https://updates.riverofglorymn.org" in after
|
|
and "AUDIO_OUTPUT=auto" in after)
|
|
|
|
print()
|
|
print("2. a room already pointed at a broker of its own")
|
|
|
|
OWN = """CHANNEL=a-channel-somebody-named
|
|
MQTT_ENABLED=true
|
|
MQTT_HOST=broker.example.invalid
|
|
MQTT_PORT=8883
|
|
MQTT_PASSWORD=leave-me
|
|
"""
|
|
|
|
after, output = migrate(OWN)
|
|
|
|
check("a deliberate address is not overwritten",
|
|
"MQTT_HOST=broker.example.invalid" in after)
|
|
check("nor a deliberate port", "MQTT_PORT=8883" in after)
|
|
check("a channel nobody here named is left alone",
|
|
"CHANNEL=a-channel-somebody-named" in after)
|
|
check("and its password is still there", "MQTT_PASSWORD=leave-me" in after)
|
|
|
|
print()
|
|
print("3. a room old enough to have no broker settings at all")
|
|
|
|
OLD = """CHANNEL=canary
|
|
UI_SCALE=1.0
|
|
"""
|
|
|
|
after, output = migrate(OLD)
|
|
|
|
check("the settings are added", "MQTT_ENABLED=true" in after
|
|
and "MQTT_HOST=10.40.0.2" in after and "MQTT_PORT=1883" in after)
|
|
check("no credentials are invented",
|
|
"MQTT_USERNAME" not in after and "MQTT_PASSWORD" not in after)
|
|
# canary is where the next development release goes. A room somebody put
|
|
# there is a room somebody meant to put there.
|
|
check("canary is left where it is", "CHANNEL=canary" in after)
|
|
|
|
after, _ = migrate("CHANNEL=classroom-v2\nUI_SCALE=1.0\n")
|
|
check("and the experiment's own channel comes home",
|
|
"CHANNEL=stable" in after)
|
|
|
|
print()
|
|
print("4. running it twice changes nothing the second time")
|
|
|
|
once, _ = migrate(BEFORE)
|
|
twice, _ = migrate(once)
|
|
check("the second run is a no-op", once == twice)
|
|
|
|
print()
|
|
print("5. a room old enough to be missing settings this release has")
|
|
|
|
# The migration knows nothing about which settings the classroom has
|
|
# gained since a room was installed, and must not: that list lives in
|
|
# classroom_config.py. What it has to do is hand the file to the shared
|
|
# helper afterwards, so a machine converges whether it arrived through the
|
|
# package's postinst or through somebody running the command by hand.
|
|
AGED = """# A room set up a long time ago. Keep this comment.
|
|
UPDATE_URL=https://updates.riverofglorymn.org
|
|
CHANNEL=a-channel-somebody-named
|
|
COMPOSITOR=hyprland
|
|
MQTT_ENABLED=false
|
|
MQTT_HOST=
|
|
MQTT_USERNAME=classroom
|
|
MQTT_PASSWORD=preserve-me
|
|
CUSTOM_SETTING=preserve-me-too
|
|
"""
|
|
|
|
after, output = migrate(AGED, shared=True)
|
|
|
|
missing = [key for key, _ in DEVICE_CONFIG_SECTIONS]
|
|
check("there are settings for this to be about", len(missing) >= 3)
|
|
|
|
for key in ("LIVE_ENABLED", "AUDIO_OUTPUT", "UI_SCALE"):
|
|
check(f"{key} is appended", f"\n{key}=" in "\n" + after)
|
|
check(f"{key} exactly once",
|
|
len(re.findall(rf"^{key}=", after, re.M)) == 1)
|
|
|
|
check("every setting this release knows about is present",
|
|
all(re.search(rf"^{key}=", after, re.M) for key in missing))
|
|
check("the room's comment survives",
|
|
"# A room set up a long time ago. Keep this comment." in after)
|
|
check("the custom setting survives", "CUSTOM_SETTING=preserve-me-too" in after)
|
|
check("the password survives", "MQTT_PASSWORD=preserve-me" in after)
|
|
check("and is still never printed", "preserve-me" not in output)
|
|
check("the username survives", "MQTT_USERNAME=classroom" in after)
|
|
check("the deliberate channel is still deliberate",
|
|
"CHANNEL=a-channel-somebody-named" in after)
|
|
check("the migration still did its own work",
|
|
"COMPOSITOR" not in after
|
|
and "MQTT_ENABLED=true" in after
|
|
and "MQTT_HOST=10.40.0.2" in after)
|
|
|
|
twice, _ = migrate(after, shared=True)
|
|
check("and running the whole thing again changes nothing", twice == after)
|
|
|
|
print()
|
|
print("6. the defaults live in one place")
|
|
|
|
check("the migration does not carry a copy of them",
|
|
not any(key in migration
|
|
for key, _ in DEVICE_CONFIG_SECTIONS
|
|
if key not in ("MQTT_ENABLED",)))
|
|
check("the script calls the shared helper",
|
|
"ensure_device_config" in script)
|
|
check("after its own migration, not before",
|
|
script.index("ensure_device_config")
|
|
> script.index("OBSOLETE_CHANNELS"))
|
|
check("with the file it was given",
|
|
"ensure_device_config(sys.argv[1])" in script)
|
|
|
|
print()
|
|
print(f"{passed} passed, {failed} failed")
|
|
sys.exit(1 if failed else 0)
|
|
PY
|