The readiness condition was playback state alone, and on a real disc VLC reports playing well before its window has taken the screen. The cover came off onto VLC's ordinary tiled startup window — black, sized to everything except the dock's reserved strip — and the disc menu only arrived afterwards. Exactly the state the cover exists to hide. Ask sway instead. VLC only takes the display when its window goes fullscreen, and the tree says when that happens. The cover now comes off when VLC reports playing or paused, its window is fullscreen, and 800ms have passed for the menu to draw its first frames. The playback check runs first because it is local and cheap; the tree is only walked once VLC claims to be playing something. The twenty second deadline is unchanged: a cover with nothing behind it is still the one outcome DVD mode must never produce. Co-Authored-By: Claude Opus 5 <[email protected]>
208 lines
7.0 KiB
Bash
Executable File
208 lines
7.0 KiB
Bash
Executable File
#!/bin/bash
|
|
set -uo pipefail
|
|
|
|
# Checks the VLC launch policy: every hotkey unset, only the classroom
|
|
# ones bound, and the control interface wired to this session only.
|
|
#
|
|
# tests/test-classroom-dvd-session.sh
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
|
|
PYTHONPATH="$ROOT/lib" python3 - "$ROOT" <<'PY'
|
|
import sys
|
|
|
|
sys.path.insert(0, sys.argv[1] + "/lib")
|
|
|
|
import classroom_vlc as vlc
|
|
|
|
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
|
|
|
|
|
|
command = vlc.player_command("/dev/sr0", 41234, "s3cret")
|
|
|
|
print("1. every VLC hotkey is unset")
|
|
|
|
check("VLC 3.0.23 defines 108 hotkeys", len(vlc.ALL_HOTKEYS) == 108,
|
|
f"got {len(vlc.ALL_HOTKEYS)}")
|
|
check("no duplicates in the hotkey list",
|
|
len(set(vlc.ALL_HOTKEYS)) == len(vlc.ALL_HOTKEYS))
|
|
|
|
unset = [a for a in command if a.startswith("--key-") and a.endswith("=")]
|
|
check("one unset per hotkey", len(unset) == len(vlc.ALL_HOTKEYS),
|
|
f"{len(unset)} unsets for {len(vlc.ALL_HOTKEYS)} hotkeys")
|
|
|
|
# The specific complaints from hardware testing.
|
|
for name in ("crop", "aspect-ratio", "vol-up", "vol-down", "vol-mute",
|
|
"subtitle-track", "audio-track", "deinterlace",
|
|
"toggle-fullscreen", "leave-fullscreen"):
|
|
check(f"{name} is unset", f"--key-{name}=" in command)
|
|
|
|
print()
|
|
print("2. only the classroom keys are bound")
|
|
|
|
bound = {a.split("=", 1)[0][len("--key-"):]: a.split("=", 1)[1]
|
|
for a in command
|
|
if a.startswith("--key-") and not a.endswith("=")}
|
|
|
|
check("bound set is exactly the classroom set",
|
|
bound == vlc.CLASSROOM_HOTKEYS, f"got {bound}")
|
|
check("Space is play/pause", bound.get("play-pause") == "Space")
|
|
check("Esc quits rather than leaving fullscreen",
|
|
bound.get("quit") == "Esc")
|
|
check("arrow keys are disc navigation, not volume",
|
|
bound.get("nav-up") == "Up" and bound.get("nav-down") == "Down")
|
|
check("Enter activates a menu item", bound.get("nav-activate") == "Enter")
|
|
check("nothing bound to volume",
|
|
not any(k.startswith("vol-") for k in bound))
|
|
check("no fullscreen toggle: DVD mode is the whole screen",
|
|
"toggle-fullscreen" not in bound and "leave-fullscreen" not in bound)
|
|
|
|
print()
|
|
print("3. unsets come before the bindings, so the bindings win")
|
|
|
|
first_binding = min(i for i, a in enumerate(command)
|
|
if a.startswith("--key-") and not a.endswith("="))
|
|
last_unset = max(i for i, a in enumerate(command) if a.endswith("=")
|
|
and a.startswith("--key-"))
|
|
check("every unset precedes every binding", last_unset < first_binding,
|
|
f"last unset {last_unset}, first binding {first_binding}")
|
|
|
|
print()
|
|
print("4. VLC's own interface is switched off")
|
|
|
|
for option in ("--qt-minimal-view", "--no-qt-fs-controller", "--no-qt-bgcone",
|
|
"--no-qt-system-tray", "--no-video-title-show", "--fullscreen",
|
|
"--qt-dark-palette"):
|
|
check(f"{option} passed", option in command)
|
|
|
|
print()
|
|
print("5. the control interface belongs to this session alone")
|
|
|
|
check("bound to loopback only",
|
|
command[command.index("--http-host") + 1] == "127.0.0.1")
|
|
check("uses the port it was given",
|
|
command[command.index("--http-port") + 1] == "41234")
|
|
check("uses the password it was given",
|
|
command[command.index("--http-password") + 1] == "s3cret")
|
|
check("passwords are not reused",
|
|
vlc.control_password() != vlc.control_password())
|
|
check("passwords are long enough to matter",
|
|
len(vlc.control_password()) >= 24)
|
|
|
|
print()
|
|
print("6. the disc is the last argument")
|
|
|
|
check("dvd:// selects dvdnav for menus", command[-1] == "dvd:///dev/sr0")
|
|
|
|
print()
|
|
print("7. the loading surface waits for a fullscreen VLC window")
|
|
|
|
import json # noqa: E402
|
|
import os # noqa: E402
|
|
import subprocess # noqa: E402
|
|
import tempfile # noqa: E402
|
|
|
|
|
|
def tree_with(app, fullscreen):
|
|
return {
|
|
"tree": {
|
|
"type": "root", "id": 1, "nodes": [{
|
|
"type": "output", "id": 2, "name": "HDMI-A-1", "nodes": [{
|
|
"type": "workspace", "id": 3, "name": "1",
|
|
"nodes": ([] if app is None else [{
|
|
"type": "con", "id": 10, "pid": 100,
|
|
"app_id": None,
|
|
"window_properties": {"class": app, "instance": app},
|
|
"focused": True,
|
|
"fullscreen_mode": 1 if fullscreen else 0,
|
|
"nodes": [], "floating_nodes": [],
|
|
}]),
|
|
"floating_nodes": [],
|
|
}], "floating_nodes": [],
|
|
}], "floating_nodes": [],
|
|
},
|
|
"workspaces": [{"name": "1", "focused": True, "visible": True}],
|
|
}
|
|
|
|
|
|
work = tempfile.mkdtemp()
|
|
os.makedirs(f"{work}/bin", exist_ok=True)
|
|
|
|
with open(f"{work}/bin/swaymsg", "w") as shim:
|
|
shim.write(
|
|
"#!/usr/bin/env python3\n"
|
|
"import json, os, sys\n"
|
|
"state = json.load(open(os.environ['MOCK_TREE']))\n"
|
|
"print(json.dumps(state['tree' if sys.argv[2] == 'get_tree'"
|
|
" else 'workspaces']))\n"
|
|
)
|
|
os.chmod(f"{work}/bin/swaymsg", 0o755)
|
|
os.environ["PATH"] = f"{work}/bin:" + os.environ["PATH"]
|
|
os.environ["MOCK_TREE"] = f"{work}/tree.json"
|
|
|
|
|
|
def set_tree(app, fullscreen):
|
|
with open(f"{work}/tree.json", "w") as handle:
|
|
json.dump(tree_with(app, fullscreen), handle)
|
|
|
|
|
|
ready = None
|
|
|
|
try:
|
|
import importlib.util
|
|
from importlib.machinery import SourceFileLoader
|
|
|
|
import gi
|
|
|
|
gi.require_version("Gtk", "3.0")
|
|
gi.require_version("GtkLayerShell", "0.1")
|
|
|
|
loader = SourceFileLoader("dvdsession", sys.argv[1] + "/scripts/classroom-dvd-session")
|
|
spec = importlib.util.spec_from_loader("dvdsession", loader)
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules["dvdsession"] = module
|
|
loader.exec_module(module)
|
|
ready = module.DvdSession.player_is_fullscreen
|
|
except Exception as error: # noqa: BLE001
|
|
print(f" skip GTK unavailable here ({type(error).__name__})")
|
|
|
|
if ready is not None:
|
|
set_tree("vlc", False)
|
|
check("a tiled VLC window is not ready", ready(None) is False)
|
|
|
|
set_tree("vlc", True)
|
|
check("a fullscreen VLC window is ready", ready(None) is True)
|
|
|
|
set_tree("Google-chrome", True)
|
|
check("someone else's fullscreen window is not VLC", ready(None) is False)
|
|
|
|
set_tree(None, False)
|
|
check("no windows at all is not ready", ready(None) is False)
|
|
|
|
check("playback state alone is no longer the condition",
|
|
"player_is_fullscreen" in module.DvdSession.watch_startup.__doc__
|
|
or "fullscreen" in module.DvdSession.watch_startup.__doc__)
|
|
check("the handover is held briefly after both agree",
|
|
500 <= module.STABILISE_MS <= 1000, f"got {module.STABILISE_MS}")
|
|
check("the safety deadline is still there",
|
|
module.LOADING_DEADLINE_SECONDS == 20)
|
|
|
|
subprocess.run(["rm", "-rf", work])
|
|
|
|
print()
|
|
print(f"passed: {passed} failed: {failed}")
|
|
sys.exit(1 if failed else 0)
|
|
PY
|