181 lines
6.0 KiB
Bash
Executable File
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"
|