test: the lab measures the update schedule on its own systemd, powered off and on
Switched on at the time: the timer fires once and is next due tomorrow. Switched off: the lab machine is really stopped across the time and started after it; the missed check runs once, 50 s after starting, and never loops. And the reboot-when-idle watcher against real kernel input, a mouse made with /dev/uinput: no reboot while it moves, a reboot once it is left alone. 11 checks. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3c40e849ea
commit
7586326902
Executable
+162
@@ -0,0 +1,162 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# When a classroom checks for updates, measured on the lab's own systemd.
|
||||||
|
#
|
||||||
|
# switched on at the time the timer fires once at its time, and not
|
||||||
|
# again that day
|
||||||
|
# switched off at the time the lab machine is really stopped across the
|
||||||
|
# time, then started: the missed check runs
|
||||||
|
# once after it boots, and does not loop
|
||||||
|
#
|
||||||
|
# A pair of test units stands in for ekklesia-classroom-update: the same
|
||||||
|
# [Timer] settings with the time moved to a minute or two from now (the
|
||||||
|
# real one would update the lab for real). The real timer's own settings,
|
||||||
|
# after installing the package, are the release gate's to check.
|
||||||
|
#
|
||||||
|
# And classroom-reboot-when-idle against real kernel input: a mouse made
|
||||||
|
# with /dev/uinput, moved and then left alone, with a harmless command in
|
||||||
|
# place of the reboot.
|
||||||
|
#
|
||||||
|
# tools/lab/update-schedule-check
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
NAME="${LAB_NAME:-classroom-v2-lab}"
|
||||||
|
LAB="$ROOT/tools/lab/lab"
|
||||||
|
IDLE="${IDLE_COMMAND:-/usr/local/bin/classroom-reboot-when-idle}"
|
||||||
|
LOG=/var/tmp/ekk-schedule-test.log
|
||||||
|
|
||||||
|
lab() { docker exec -i "$NAME" bash -c "$1"; }
|
||||||
|
|
||||||
|
passed=0
|
||||||
|
failed=0
|
||||||
|
|
||||||
|
judge() {
|
||||||
|
if eval "$2"; then
|
||||||
|
printf ' \033[32mPASS\033[0m %s\n' "$1"
|
||||||
|
passed=$((passed + 1))
|
||||||
|
else
|
||||||
|
printf ' \033[31mFAIL\033[0m %s\n' "$1"
|
||||||
|
failed=$((failed + 1))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# A timer like the real one, due at HH:MM:SS local time, with DELAY spread.
|
||||||
|
install_test_timer() { # WHEN DELAY
|
||||||
|
lab "cat > /etc/systemd/system/ekk-schedule-test.service <<'UNIT'
|
||||||
|
[Unit]
|
||||||
|
Description=Stand-in for the update check
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/bin/sh -c 'echo \"fired \$(date +%%s) boot \$(cut -d. -f1 /proc/uptime)\" >> $LOG'
|
||||||
|
UNIT
|
||||||
|
cat > /etc/systemd/system/ekk-schedule-test.timer <<UNIT
|
||||||
|
[Unit]
|
||||||
|
Description=Stand-in for the update check
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-* $1
|
||||||
|
RandomizedDelaySec=$2
|
||||||
|
Persistent=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
UNIT
|
||||||
|
rm -f $LOG /var/lib/systemd/timers/stamp-ekk-schedule-test.timer
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now ekk-schedule-test.timer >/dev/null 2>&1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fired() { lab "grep -c fired $LOG 2>/dev/null || echo 0" | tail -1; }
|
||||||
|
local_in() { lab "date -d '+$1 seconds' +%H:%M:%S"; } # local time SECONDS from now
|
||||||
|
|
||||||
|
echo "switched on at the time"
|
||||||
|
due=$(local_in 60)
|
||||||
|
install_test_timer "$due" 0
|
||||||
|
judge "the stand-in is waiting for $due, local time" \
|
||||||
|
"lab 'systemctl list-timers ekk-schedule-test.timer --no-legend | grep -q ekk-schedule-test'"
|
||||||
|
sleep 80
|
||||||
|
judge "at $due it checked, once" '[[ $(fired) -eq 1 ]]'
|
||||||
|
sleep 60
|
||||||
|
judge "and not again (a minute later, still once)" '[[ $(fired) -eq 1 ]]'
|
||||||
|
next=$(lab "systemctl show ekk-schedule-test.timer -p NextElapseUSecRealtime --value")
|
||||||
|
tomorrow=$(lab "date -d tomorrow +%Y-%m-%d")
|
||||||
|
judge "its next check is tomorrow at $due ($next)" '[[ "$next" == *"$tomorrow $due"* ]]'
|
||||||
|
|
||||||
|
echo "switched off at the time"
|
||||||
|
due=$(local_in 90)
|
||||||
|
install_test_timer "$due" 60
|
||||||
|
lab "systemctl start ekk-schedule-test.timer; sync"
|
||||||
|
# A stamp from a previous check, as every classroom has one: last checked
|
||||||
|
# yesterday. The catch-up is decided against it.
|
||||||
|
lab "touch -d yesterday /var/lib/systemd/timers/stamp-ekk-schedule-test.timer"
|
||||||
|
echo " powering the lab off before $due"
|
||||||
|
docker stop -t 20 "$NAME" >/dev/null
|
||||||
|
sleep 150
|
||||||
|
echo " and on again, after it"
|
||||||
|
"$LAB" reboot >/dev/null 2>&1
|
||||||
|
# When the lab machine really started (inside a container /proc/uptime is
|
||||||
|
# the host's, so this is asked of docker).
|
||||||
|
booted=$(date -d "$(docker inspect -f '{{.State.StartedAt}}' "$NAME")" +%s)
|
||||||
|
judge "while it was off, nothing ran" '[[ $(lab "grep -c fired $LOG 2>/dev/null || echo 0" | tail -1) -le 1 ]]'
|
||||||
|
for _ in $(seq 1 24); do [[ $(fired) -ge 1 ]] && break; sleep 5; done
|
||||||
|
judge "switched on after $due: the missed check ran after it booted ($(lab "tail -1 $LOG"))" '[[ $(fired) -eq 1 ]]'
|
||||||
|
after=$(( $(lab "awk '{print \$2}' $LOG | head -1") - booted ))
|
||||||
|
judge "$after s after it started: once it was up, within the minute of spread" '(( after >= 0 && after <= 150 ))'
|
||||||
|
sleep 90
|
||||||
|
judge "and only once: no loop after booting (90 s later, still once)" '[[ $(fired) -eq 1 ]]'
|
||||||
|
lab "systemctl disable --now ekk-schedule-test.timer >/dev/null 2>&1; rm -f /etc/systemd/system/ekk-schedule-test.* $LOG /var/lib/systemd/timers/stamp-ekk-schedule-test.timer; systemctl daemon-reload"
|
||||||
|
|
||||||
|
echo "reboot only when nobody is using it"
|
||||||
|
docker cp "$ROOT/scripts/classroom-reboot-when-idle" "$NAME:/opt/classroom-reboot-when-idle" >/dev/null
|
||||||
|
lab "chmod 755 /opt/classroom-reboot-when-idle; rm -f /tmp/rebooted"
|
||||||
|
[[ "$IDLE" == /usr/local/bin/* ]] && lab "test -x $IDLE" || IDLE=/opt/classroom-reboot-when-idle
|
||||||
|
cat > /tmp/update-schedule-mouse.py <<'PY'
|
||||||
|
"""A mouse made with /dev/uinput: real kernel input events. Moves for N s, then stays."""
|
||||||
|
import fcntl, os, struct, sys, time
|
||||||
|
UI_SET_EVBIT, UI_SET_RELBIT, UI_SET_KEYBIT = 0x40045564, 0x40045566, 0x40045565
|
||||||
|
UI_DEV_CREATE, UI_DEV_DESTROY = 0x5501, 0x5502
|
||||||
|
EV_SYN, EV_KEY, EV_REL, REL_X, BTN_LEFT = 0, 1, 2, 0, 0x110
|
||||||
|
fd = os.open("/dev/uinput", os.O_WRONLY | os.O_NONBLOCK)
|
||||||
|
for ioctl, value in ((UI_SET_EVBIT, EV_KEY), (UI_SET_KEYBIT, BTN_LEFT), (UI_SET_EVBIT, EV_REL), (UI_SET_RELBIT, REL_X)):
|
||||||
|
fcntl.ioctl(fd, ioctl, value)
|
||||||
|
os.write(fd, struct.pack("80sHHHHi", b"lab-classroom-mouse", 3, 1, 1, 1, 0) + b"\0" * (4 * 64 * 4))
|
||||||
|
fcntl.ioctl(fd, UI_DEV_CREATE)
|
||||||
|
time.sleep(1)
|
||||||
|
def event(kind, code, value):
|
||||||
|
now = time.time()
|
||||||
|
os.write(fd, struct.pack("llHHi", int(now), int(now % 1 * 1e6), kind, code, value))
|
||||||
|
end = time.monotonic() + float(sys.argv[1])
|
||||||
|
while time.monotonic() < end:
|
||||||
|
event(EV_REL, REL_X, 1); event(EV_SYN, 0, 0)
|
||||||
|
time.sleep(2)
|
||||||
|
print("mouse left alone", flush=True)
|
||||||
|
time.sleep(float(sys.argv[2]))
|
||||||
|
fcntl.ioctl(fd, UI_DEV_DESTROY)
|
||||||
|
PY
|
||||||
|
docker cp /tmp/update-schedule-mouse.py "$NAME:/opt/update-schedule-mouse.py" >/dev/null
|
||||||
|
rm -f /tmp/update-schedule-mouse.py
|
||||||
|
docker exec -d "$NAME" python3 /opt/update-schedule-mouse.py 40 90
|
||||||
|
sleep 2
|
||||||
|
# The kernel makes the device; on a classroom machine devtmpfs gives it a
|
||||||
|
# node in /dev/input. The lab's /dev is a plain tmpfs, so the node is made
|
||||||
|
# here, from what sysfs says, as devtmpfs would.
|
||||||
|
lab 'for e in /sys/class/input/event*; do
|
||||||
|
[[ "$(cat $e/device/name)" == lab-classroom-mouse ]] || continue
|
||||||
|
IFS=: read -r major minor < $e/dev
|
||||||
|
mknod /dev/input/$(basename $e) c $major $minor 2>/dev/null
|
||||||
|
echo $(basename $e) > /tmp/lab-mouse-node
|
||||||
|
done'
|
||||||
|
sleep 1
|
||||||
|
docker exec -d "$NAME" sh -c "$IDLE --idle-seconds 25 --check-seconds 2 --reboot-command touch /tmp/rebooted > /tmp/idle.log 2>&1"
|
||||||
|
sleep 35
|
||||||
|
judge "the mouse moved every 2 s for 40 s: no reboot, against a 25 s limit" "! lab 'test -e /tmp/rebooted'"
|
||||||
|
judge "it was watching real devices ($(lab 'grep -o "watching [0-9]* input devices" /tmp/idle.log'))" \
|
||||||
|
"lab 'grep -qE \"watching [1-9][0-9]* input devices\" /tmp/idle.log'"
|
||||||
|
sleep 40
|
||||||
|
judge "left alone for longer than the limit: it rebooted" "lab 'test -e /tmp/rebooted'"
|
||||||
|
lab "pkill -f '[u]pdate-schedule-mouse'; rm -f /dev/input/\$(cat /tmp/lab-mouse-node 2>/dev/null) /tmp/lab-mouse-node /tmp/rebooted /opt/update-schedule-mouse.py /opt/classroom-reboot-when-idle"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "$passed passed, $failed failed"
|
||||||
|
[[ "$failed" -eq 0 ]]
|
||||||
Reference in New Issue
Block a user