#!/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