Files
KNELDoorman/tests/run-tests.sh
T
mrcharles 3a9124b16d feat(doorman): badge listener + decoder TDD + deploy scaffolding [#355]
Modern successor of the 2018 Perl doorman (snapshot in legacy/):
bash+coreutils HID decoder, HA webhook dispatch, systemd unit.
Offline decoder suite green (9 tests); shellcheck zero-warning.

Umbrella #345. Work item: https://projects.knownelement.com/issues/355
2026-09-02 21:11:13 -05:00

187 lines
6.0 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# tests/run-tests.sh — doorman decoder unit tests (offline, no hardware).
#
# Builds synthetic /dev/input event streams (the exact binary records the
# kernel emits) and asserts bin/doorman.sh --selftest decodes them the way
# the 2018 Perl doorman did: digits 1-9 = HID usages 30-38, 0 = 39,
# Enter = 40, each key arriving as a down/up MSC_SCAN pair that the
# parity logic collapses to one digit.
#
# Usage: bash tests/run-tests.sh
#
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BIN="$ROOT/bin/doorman.sh"
TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
pass=0
fail=0
ok() { printf 'ok - %s\n' "$1"; pass=$((pass + 1)); }
no() { printf 'FAIL - %s\n' "$1"; fail=$((fail + 1)); }
# ---------------------------------------------------------------------------
# Fixture builders
# ---------------------------------------------------------------------------
hex() { printf '\\x%02x' "$1"; }
# emit_event <width> <type> <code> <value> — one input_event record, LE.
emit_event() {
local w="$1" t="$2" c="$3" v="$4" tv
if [ "$w" -eq 24 ]; then tv=16; else tv=8; fi
# shellcheck disable=SC2046 # %.0s consumes the seq args; emits tv NULs
printf '\x00%.0s' $(seq 1 "$tv") > "$TMP/ev.bin"
{
# type and code are __u16 LE, value is __s32 LE
printf '%b' "$(hex $((t & 255)))$(hex $(((t >> 8) & 255)))$(hex $((c & 255)))$(hex $(((c >> 8) & 255)))"
printf '%b' "$(hex $((v & 255)))$(hex $(((v >> 8) & 255)))$(hex $(((v >> 16) & 255)))$(hex $(((v >> 24) & 255)))"
} >> "$TMP/ev.bin"
cat "$TMP/ev.bin"
}
# emit_scan <width> <usage> — one keypress: down + up MSC_SCAN + SYN filler.
emit_scan() {
emit_event "$1" 4 4 "$2"
emit_event "$1" 4 4 "$2"
emit_event "$1" 0 0 0
}
# emit_badge <width> <digits...> — each digit + terminating Enter.
emit_badge() {
local w="$1"; shift
local d u
for d in "$@"; do
if [ "$d" = "ENTER" ]; then u=40; else u=$((29 + 10#$d)); fi
emit_scan "$w" "$u"
done
}
# emit_digits <width> <digits> — one scan event pair per digit (0 = usage 39,
# 1-9 = usages 30-38). Takes the badge string itself, not precomputed usages.
emit_digits() {
local w="$1" s="$2" i d u
for ((i = 0; i < ${#s}; i++)); do
d="${s:$i:1}"
if [ "$d" = "0" ]; then u=39; else u=$((29 + 10#$d)); fi
emit_scan "$w" "$u"
done
}
# emit_enter <width> — the Enter terminator keypress.
emit_enter() { emit_scan "$1" 40; }
run_selftest() { # run_selftest <fixture> [width]
local w="${2:-24}"
"$BIN" --selftest "$1" --width "$w" 2>/dev/null
}
assert_ids() { # assert_ids <desc> <fixture> <expected...> [--width N]
local desc="$1" fixture="$2"
shift 2
local w=24
local -a exp=()
while [ $# -gt 0 ]; do
if [ "$1" = "--width" ]; then
w="$2"
shift 2
else
exp+=("$1")
shift
fi
done
local got expected
got="$(run_selftest "$fixture" "$w")"
expected="$(printf '%s\n' "${exp[@]}")"
if [ "$got" = "$expected" ]; then
ok "$desc"
else
no "$desc"
printf ' expected: %s\n' "$(printf '%s | ' "${exp[@]}")"
printf ' got: %s\n' "$(printf '%s' "$got" | tr '\n' ' ')"
fi
}
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
# T1: two full 10-digit badges + a repeated-digit badge ("11" exercises the
# down/up parity state machine on consecutive identical scan codes).
f1="$TMP/f1.bin"
{
emit_digits 24 "0009399422"; emit_enter 24
emit_digits 24 "0009106067"; emit_enter 24
emit_digits 24 "11"; emit_enter 24
} > "$f1"
assert_ids "T1 decodes two 10-digit badges + repeated-digit badge" "$f1" \
"0009399422" "0009106067" "11"
# T2: garbage MSC_SCAN usage mid-badge (e.g. media key) must not corrupt
# or truncate the scan (2018 behavior: log+skip, id preserved).
f2="$TMP/f2.bin"
{
emit_digits 24 "000"
emit_scan 24 42 # garbage usage (backspace) — must be skipped
emit_digits 24 "9399422"
emit_enter 24
} > "$f2"
assert_ids "T2 garbage scan usage mid-badge is skipped" "$f2" "0009399422"
# T3: lone Enter (empty scan) emits nothing.
f3="$TMP/f3.bin"
emit_enter 24 > "$f3"
got="$(run_selftest "$f3")"
if [ -z "$got" ]; then ok "T3 lone Enter emits no badge"; else no "T3 lone Enter emits no badge (got: $got)"; fi
# T4: non-MSC events (EV_KEY type 1) around a badge are ignored.
f4="$TMP/f4.bin"
{
emit_event 24 1 30 1; emit_event 24 1 30 0
emit_digits 24 "0"; emit_enter 24
emit_event 24 1 30 1; emit_event 24 1 30 0
} > "$f4"
assert_ids "T4 EV_KEY events ignored, MSC_SCAN decoded" "$f4" "0"
# T5: truncated trailing record does not hang or corrupt output.
f5="$TMP/f5.bin"
{
emit_digits 24 "7"; emit_enter 24
emit_event 24 4 4 39
printf '\x00\x00\x00' # 3-byte orphan tail
} > "$f5"
assert_ids "T5 truncated tail record ignored" "$f5" "7"
# T6: 16-byte records (32-bit boards, e.g. older Pis) decode identically.
f6="$TMP/f6.bin"
{ emit_digits 16 "0"; emit_enter 16; } > "$f6"
assert_ids "T6 16-byte record width supported" "$f6" "0" --width 16
# T7: all-zero stream (idle bus) emits nothing.
f7="$TMP/f7.bin"
for _ in 1 2 3 4 5; do emit_event 24 0 0 0; done > "$f7"
got="$(run_selftest "$f7")"
if [ -z "$got" ]; then ok "T7 idle all-zero stream emits nothing"; else no "T7 idle stream (got: $got)"; fi
# T8: missing fixture file fails cleanly, not silently.
if "$BIN" --selftest "$TMP/does-not-exist.bin" >/dev/null 2>&1; then
no "T8 missing fixture exits nonzero"
else
ok "T8 missing fixture exits nonzero"
fi
# T9: known 2018 backdoor badges replay end-to-end (regression anchor).
f9="$TMP/f9.bin"
{
emit_digits 24 "0009399422"; emit_enter 24
emit_digits 24 "0009106067"; emit_enter 24
} > "$f9"
assert_ids "T9 2018 backdoor badges decode verbatim" "$f9" \
"0009399422" "0009106067"
# ---------------------------------------------------------------------------
printf '\n%d passed, %d failed\n' "$pass" "$fail"
[ "$fail" -eq 0 ]