Files
stormux/tests/test-select-speech-module.sh

181 lines
6.0 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
repoRoot="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
selector="$repoRoot/pi4/files/usr/local/lib/stormux/select-speech-module"
tempDir="$(mktemp -d)"
mockBin="$tempDir/bin"
testCount=0
cleanup() {
rm -rf "$tempDir"
}
trap cleanup EXIT
mkdir -p "$mockBin" "$tempDir/runtime"
cat > "$mockBin/sudo" <<'EOF'
#!/usr/bin/env bash
if [[ "${1:-}" == "-A" ]]; then
shift
fi
if [[ "${MOCK_SUDO_FAIL_WRITE:-no}" == "yes" && "$*" == *"--write-selection"* ]]; then
exit 1
fi
exec "$@"
EOF
cat > "$mockBin/spd-say" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "$*" >> "$MOCK_LOG"
if [[ "${1:-}" == "-O" ]]; then
cat "$MOCK_MODULES"
exit "${MOCK_DISCOVERY_STATUS:-0}"
fi
exit 0
EOF
cat > "$mockBin/socat" <<'EOF'
#!/usr/bin/env bash
read -r command
printf 'socat %s: %s\n' "$*" "$command" >> "$MOCK_LOG"
EOF
cat > "$mockBin/systemctl" <<'EOF'
#!/usr/bin/env bash
printf 'systemctl %s\n' "$*" >> "$MOCK_LOG"
if [[ "${1:-}" == "is-active" ]]; then
[[ "${MOCK_FENRIR_ACTIVE:-no}" == "yes" ]]
exit
fi
exit 0
EOF
chmod 755 "$mockBin"/*
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
assert_equals() {
local expected="$1"
local actual="$2"
local description="$3"
[[ "$actual" == "$expected" ]] || fail "$description (expected '$expected', got '$actual')"
}
assert_file_contains() {
local filePath="$1"
local pattern="$2"
local description="$3"
grep -qE -- "$pattern" "$filePath" || fail "$description"
}
assert_file_absent() {
local filePath="$1"
local description="$2"
[[ ! -e "$filePath" ]] || fail "$description"
}
reset_case() {
caseDir="$tempDir/case-$testCount"
testCount=$((testCount + 1))
mkdir -p "$caseDir/runtime"
configFile="$caseDir/speechd.conf"
selectionFile="$caseDir/selected"
modulesFile="$caseDir/modules"
mockLog="$caseDir/mock.log"
: > "$mockLog"
printf 'OUTPUT MODULES\nrhvoice\nopenevv\nespeak-ng\n' > "$modulesFile"
printf '# Speech Dispatcher\nDefaultModule rhvoice\n' > "$configFile"
}
run_selector() {
local input="$1"
printf '%b' "$input" | env \
PATH="$mockBin:$PATH" \
MOCK_LOG="$mockLog" \
MOCK_MODULES="$modulesFile" \
MOCK_DISCOVERY_STATUS="${MOCK_DISCOVERY_STATUS:-0}" \
MOCK_FENRIR_ACTIVE="${MOCK_FENRIR_ACTIVE:-no}" \
MOCK_SUDO_FAIL_WRITE="${MOCK_SUDO_FAIL_WRITE:-no}" \
STORMUX_SPEECH_CONFIG="$configFile" \
STORMUX_RUNTIME_DIR="$caseDir/runtime" \
STORMUX_SPD_SAY=spd-say \
STORMUX_SOCAT=socat \
STORMUX_SYSTEMCTL=systemctl \
"$selector" --selection-file "$selectionFile" >/dev/null
}
# Enter accepts the configured initial module and collapses duplicate directives.
reset_case
printf 'DefaultModule rhvoice\n# DefaultModule espeak-ng\nDefaultModule openevv\n' > "$configFile"
MOCK_FENRIR_ACTIVE=yes run_selector '\n'
assert_equals 1 "$(grep -c '^DefaultModule ' "$configFile")" "initial selection should leave one active default"
assert_file_contains "$configFile" '^DefaultModule rhvoice$' "initial selection should retain rhvoice"
assert_equals rhvoice "$(< "$selectionFile")" "initial selection should be recorded"
assert_file_contains "$mockLog" 'speech#enabled=False' "active Fenrir speech should be muted"
assert_file_contains "$mockLog" 'speech#enabled=True' "active Fenrir speech should be restored"
assert_file_contains "$mockLog" 'Use up and down arrows to select the default voice for this computer\.' "the selector should voice its own instructions"
if grep -q '^systemctl restart ' "$mockLog"; then
fail "the selector should defer the Fenrir restart until after layout selection"
fi
# Down previews and selects the next module.
reset_case
run_selector 'x\033[B\n'
assert_file_contains "$mockLog" '^-o openevv This is openevv\.' "Down should preview OpenEVV"
assert_file_contains "$configFile" '^DefaultModule openevv$' "Down then Enter should select OpenEVV"
# Up wraps from the first module to the last.
reset_case
run_selector '\033[A\n'
assert_file_contains "$mockLog" '^-o espeak-ng This is espeak-ng\.' "Up should wrap and preview eSpeak NG"
assert_file_contains "$configFile" '^DefaultModule espeak-ng$' "wrapped module should persist"
# Escape retains the prior file and does not create an installer selection.
reset_case
cp "$configFile" "$caseDir/original.conf"
run_selector '\033'
cmp -s "$configFile" "$caseDir/original.conf" || fail "Escape should retain the configuration"
assert_file_absent "$selectionFile" "Escape should not record an installer selection"
# Empty discovery fails safely.
reset_case
printf 'OUTPUT MODULES\n' > "$modulesFile"
cp "$configFile" "$caseDir/original.conf"
run_selector '\n'
cmp -s "$configFile" "$caseDir/original.conf" || fail "empty discovery should retain the configuration"
assert_file_absent "$selectionFile" "empty discovery should not record a selection"
# Unsafe names are rejected rather than passed to spd-say or configuration.
reset_case
printf 'OUTPUT MODULES\nbad name\n;command\nopenevv\n' > "$modulesFile"
run_selector '\n'
assert_file_contains "$configFile" '^DefaultModule openevv$' "the remaining safe module should be selected"
if grep -qE 'bad name|;command' "$mockLog"; then
fail "unsafe module names should not reach spd-say"
fi
# A missing configuration simulates failed persistence and remains nonfatal.
reset_case
rm -f "$configFile"
MOCK_FENRIR_ACTIVE=yes run_selector '\n'
assert_file_absent "$selectionFile" "failed persistence should not record a selection"
assert_file_contains "$mockLog" 'selection failed' "failed persistence should be announced"
assert_file_contains "$mockLog" 'speech#enabled=True' "failed persistence should restore Fenrir speech"
# Failure to record the live choice also rolls the system configuration back.
reset_case
cp "$configFile" "$caseDir/original.conf"
MOCK_SUDO_FAIL_WRITE=yes run_selector '\033[B\n'
cmp -s "$configFile" "$caseDir/original.conf" || fail "selection-file failure should roll back the configuration"
assert_file_absent "$selectionFile" "selection-file failure should leave no selection"
printf 'PASS: %s speech selector cases\n' "$testCount"