Browser or terminal controller drives an ESP32-S3 that acts as a Logitech G815 into a target machine running no software. Disarmed by default; arming requires the passphrase, the board button, or the serial console, and every fail-safe path cancels in-flight work. - src/: firmware with pure, host-testable modules (keystate, proto, text, ws_codec, sha1), the WebSocket control server, and the served page - tests/: host suites for the pure modules, relayctl parity, and page JS - tools/: relayctl terminal controller, serial console, bootloader helper, and an evdev target watcher - platformio.ini: bridge (USB HID) and console (no-HID) environments - README.md: single source of truth for behaviour, protocol and safety LLM-Generated: true
144 lines
4.8 KiB
Bash
Executable File
144 lines
4.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Host tests for keyrelay's pure logic modules.
|
|
#
|
|
# tests/run_tests.sh build and run every suite
|
|
# tests/run_tests.sh clean remove the build directory
|
|
#
|
|
# The modules under test (src/keystate.h, src/proto.h, src/text.h, src/ws_codec.h,
|
|
# src/sha1.h) have no Arduino or ESP-IDF dependency by design, so the exact code that
|
|
# runs on the board is compiled and exercised here before anything is flashed.
|
|
#
|
|
# A C++ compiler is not always installed on a workstation, so the runner picks one:
|
|
# $CXX if you set it, it wins
|
|
# g++ on PATH native, fastest
|
|
# a local container image podman + the first image that has g++ (see images
|
|
# below), with this repo bind-mounted read-write into
|
|
# /work -- no system packages installed for a test run
|
|
set -uo pipefail
|
|
|
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
root="$(dirname "$here")"
|
|
out="$here/build"
|
|
|
|
if [[ "${1:-}" == "clean" ]]; then
|
|
rm -rf "$out"
|
|
echo "cleaned $out"
|
|
exit 0
|
|
fi
|
|
|
|
# Candidate images, tried in order, that already exist locally. Add your own here.
|
|
images=(
|
|
"localhost/ai-sandbox:ubuntu-9a08b1d500b0"
|
|
"docker.io/library/gcc:14"
|
|
)
|
|
|
|
suites=(test_keystate test_proto test_text test_ws_codec)
|
|
|
|
build_and_run() {
|
|
local cxx="$1"
|
|
local prefix=("$cxx")
|
|
local flags=(-std=gnu++17 -O1 -g -Wall -Wextra -Wno-unused-function -Isrc -Itests)
|
|
|
|
mkdir -p "$out"
|
|
if [[ -n "${SANITIZE:-}" ]]; then
|
|
flags+=(-fsanitize=address,undefined -fno-omit-frame-pointer)
|
|
echo "sanitizers: on"
|
|
fi
|
|
|
|
local failed=0 suite
|
|
for suite in "${suites[@]}"; do
|
|
if ! "${prefix[@]}" "${flags[@]}" "tests/$suite.cpp" -o "$out/$suite" \
|
|
2>"$out/$suite.log"; then
|
|
echo "BUILD FAILED: $suite"
|
|
cat "$out/$suite.log"
|
|
failed=1
|
|
continue
|
|
fi
|
|
[[ -s "$out/$suite.log" ]] && cat "$out/$suite.log"
|
|
"$out/$suite" || failed=1
|
|
done
|
|
|
|
# The Python side (the CLI client's WebSocket codec, and its key table against
|
|
# the one embedded in the controller page) needs only stdlib.
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
python3 tests/test_relayctl.py || failed=1
|
|
else
|
|
echo "python3 not found: skipped tests/test_relayctl.py"
|
|
fi
|
|
|
|
return $failed
|
|
}
|
|
|
|
# The controller page is a C++ raw string, so no compiler looks at its JavaScript and
|
|
# a typo there fails in somebody's browser instead of in a build. Node runs it -- on
|
|
# the host, deliberately, because the container that supplies g++ need not have node.
|
|
run_page_suite() {
|
|
if command -v node >/dev/null 2>&1; then
|
|
node "$here/test_page.js" || return 1
|
|
else
|
|
echo "node not found: skipped tests/test_page.js"
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
if [[ "${1:-}" == "--inside" ]]; then
|
|
cd "$root" || exit 2
|
|
# The container has g++ but not necessarily node; the page suite skips itself.
|
|
build_and_run "${CXX:-g++}"
|
|
rc=$?
|
|
if [[ -z "${KR_QUIET_RESULT:-}" ]]; then
|
|
if ((rc)); then echo "RESULT: FAILED"; else echo "RESULT: all suites passed"; fi
|
|
fi
|
|
exit $rc
|
|
fi
|
|
|
|
if [[ -n "${CXX:-}" ]]; then
|
|
echo "compiler: \$CXX = $CXX"
|
|
(cd "$root" && build_and_run "$CXX")
|
|
rc=$?
|
|
(cd "$root" && run_page_suite) || rc=1
|
|
if ((rc)); then echo "RESULT: FAILED"; else echo "RESULT: all suites passed"; fi
|
|
exit $rc
|
|
fi
|
|
|
|
if command -v g++ >/dev/null 2>&1; then
|
|
echo "compiler: $(command -v g++)"
|
|
(cd "$root" && build_and_run g++)
|
|
rc=$?
|
|
(cd "$root" && run_page_suite) || rc=1
|
|
if ((rc)); then echo "RESULT: FAILED"; else echo "RESULT: all suites passed"; fi
|
|
exit $rc
|
|
fi
|
|
|
|
if command -v podman >/dev/null 2>&1; then
|
|
for img in "${images[@]}"; do
|
|
if ! timeout 60 podman image exists "$img" 2>/dev/null; then
|
|
continue
|
|
fi
|
|
if ! timeout 60 podman run --rm --entrypoint sh "$img" -c 'command -v g++' \
|
|
>/dev/null 2>&1; then
|
|
continue
|
|
fi
|
|
echo "compiler: g++ inside $img (repo mounted at /work)"
|
|
# :z relabels for SELinux; the build output lands in tests/build as usual.
|
|
# KR_QUIET_RESULT: the host prints the one summary, after its own page suite.
|
|
podman run --rm --entrypoint bash -v "$root:/work:z" \
|
|
-e "SANITIZE=${SANITIZE:-}" -e KR_QUIET_RESULT=1 \
|
|
"$img" /work/tests/run_tests.sh --inside
|
|
rc=$?
|
|
(cd "$root" && run_page_suite) || rc=1
|
|
if ((rc)); then echo "RESULT: FAILED"; exit 1; fi
|
|
echo "RESULT: all suites passed"
|
|
exit 0
|
|
done
|
|
fi
|
|
|
|
cat >&2 <<EOF
|
|
No C++ compiler found.
|
|
|
|
install one e.g. sudo dnf install gcc-c++ (then re-run)
|
|
or set \$CXX e.g. CXX=/path/to/g++ tests/run_tests.sh
|
|
or add an image add a local podman image with g++ to images[] above
|
|
EOF
|
|
exit 2
|