#!/bin/bash # # command_option_test.sh — Command-line option parsing (mac/argv.cpp). # # The commands' own argument handling, which no suite covered: argv_test.cpp # exercises Argv but asserts nothing, so two defects lived there unnoticed. # # * An option declared with opt() takes a value. Written LAST with nothing # after it -- "kdesc -v", "ktext doc.kt -t" -- parse_optional() read one # past the end of the word vector and the command died with SIGSEGV, # naming nothing. Two bounds checks sat commented out at that spot; they # would have returned a half-parsed option instead of reporting the # mistake. It is now a located argument error. # * kdesc and kdiag printed an error and then exited 0, so a script could # not tell a failed run from a successful one. Both return 1 now, as # ktext already did. # # Also here: two required positional arguments. parse_positional() consumes # one per required name, but regex_split_prefix() requires its match at # position 0 and returned the remainder with the separating space intact, so # the second was always "not found". Latent, because no shipped command # declares two -- tst/argv_test.cpp is the only program that does. # # Usage: ./command_option_test.sh (needs KLAMMERTEXT_HOME set; commands on PATH) # Exit code: 0 if all tests pass, 1 otherwise. PASS=0 FAIL=0 K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set} TSTDIR="$(cd "$(dirname "$0")" && pwd)" red=$'\033[31m' green=$'\033[32m' bold=$'\033[1m' reset=$'\033[0m' # A signal death is not an error exit: 128+n, and the point of these tests is # the difference. Report it distinctly so a crash can never read as a pass. died_by_signal() { [ "$1" -gt 128 ]; } # error NAME PATTERN CMD... — nonzero exit, no signal, PATTERN in the output. error() { local name="$1" pattern="$2"; shift 2 local out status out=$("$@" 2>&1); status=$? if died_by_signal $status; then echo "${red}FAIL${reset} $name — died by signal $((status-128))"; FAIL=$((FAIL+1)); return fi if [ $status -eq 0 ]; then echo "${red}FAIL${reset} $name — reported an error but exited 0"; FAIL=$((FAIL+1)); return fi if printf '%s' "$out" | grep -qF -- "$pattern"; then echo "${green}PASS${reset} $name"; PASS=$((PASS+1)) else echo "${red}FAIL${reset} $name — expected [$pattern]" echo " got: $(printf '%s' "$out" | head -2)"; FAIL=$((FAIL+1)) fi } # ok NAME CMD... — exits 0 and does not die by signal. ok() { local name="$1"; shift local status "$@" >/dev/null 2>&1; status=$? if died_by_signal $status; then echo "${red}FAIL${reset} $name — died by signal $((status-128))"; FAIL=$((FAIL+1)) elif [ $status -ne 0 ]; then echo "${red}FAIL${reset} $name — exit $status"; FAIL=$((FAIL+1)) else echo "${green}PASS${reset} $name"; PASS=$((PASS+1)) fi } echo "${bold}Command option tests${reset}" echo "====================" echo echo "-- an option written last, with no value --" error " 1. kdesc -v" "needs a value" kdesc -v error " 2. kdesc with a flag first" "needs a value" kdesc --katoms -v error " 3. kdiag -v" "needs a value" kdiag -v error " 4. ktext -t" "needs a value" ktext -s '@i-x' -t error " 5. the message names the option" "-v " kdesc -v error " 6. ... and how to get the usage" "for the list of arguments" kdesc -v echo echo "-- the same options WITH a value still work --" # NOT "kdesc -v 1": show_usage() treats exactly " -v " as a # request for the usage text, which exits 1 by design. Give it another flag. ok " 7. kdesc -c -v 1" kdesc -c -v 1 ok " 8. kdesc --katoms -v 1" kdesc --katoms -v 1 ok " 9. kdiag -v 1 '@i-x'" kdiag -v 1 '@i-x' ok "10. ktext -t html" ktext -s '@i-x' -t html -d echo echo "-- an error exit is nonzero, not a printed message and exit 0 --" error "11. kdesc reports failure" "needs a value" kdesc -v error "12. kdiag reports failure" "needs a value" kdiag -v echo echo "-- two required positional arguments --" # argv_test is the only program declaring two; it prints what it parsed. if [ -x "$TSTDIR/argv_test" ]; then out=$("$TSTDIR/argv_test" first second --bool 2>&1 | sed 's/\x1b\[[0-9;]*m//g') for expect in " : first" " : second"; do if printf '%s' "$out" | grep -qF "$expect"; then echo "${green}PASS${reset} 13/14. positional [$expect]"; PASS=$((PASS+1)) else echo "${red}FAIL${reset} 13/14. positional [$expect] not parsed"; FAIL=$((FAIL+1)) fi done else echo "SKIP 13/14. two positionals (argv_test not built; run make -C tst)" fi echo echo "-- the output policy: three categories, two streams --" # The three commands display text in exactly three cases: # 1. logging under "-v" > 0 -> STDERR # 2. an error before termination -> STDERR # 3. output the user asked for -> STDOUT # For ktext, category 3 is a DOCUMENT that may be piped, so nothing else may # share the stream. It did: K::log and msg() both wrote to stdout, so # "ktext -d -v 1 > doc.txt" put the whole argument dump inside the document. OUTF=$(mktemp /tmp/kout.XXXXXX); ERRF=$(mktemp /tmp/kerr.XXXXXX) trap 'rm -f "$OUTF" "$ERRF"' EXIT ktext --klammersets none -s 'hello' -d -v 1 > "$OUTF" 2> "$ERRF" if [ "$(cat "$OUTF")" = "hello" ]; then echo "${green}PASS${reset} 15. stdout under -d -v 1 is the document, nothing else" PASS=$((PASS+1)) else echo "${red}FAIL${reset} 15. stdout carried more than the document" echo " got: $(head -3 "$OUTF")"; FAIL=$((FAIL+1)) fi if [ -s "$ERRF" ]; then echo "${green}PASS${reset} 16. ... and the -v 1 logging went to stderr"; PASS=$((PASS+1)) else echo "${red}FAIL${reset} 16. -v 1 produced no stderr"; FAIL=$((FAIL+1)) fi # -v 0 adds nothing anywhere: silence is the default, on both streams. ktext --klammersets none -s 'hello' -d > "$OUTF" 2> "$ERRF" if [ "$(cat "$OUTF")" = "hello" ] && [ ! -s "$ERRF" ]; then echo "${green}PASS${reset} 17. -v 0 is silent on both streams"; PASS=$((PASS+1)) else echo "${red}FAIL${reset} 17. -v 0 was not silent (stderr: $(head -2 "$ERRF"))" FAIL=$((FAIL+1)) fi # An error is category 2: stderr, and nothing on stdout to confuse a pipe. ktext --klammersets none -s '@nosuch x @' -d > "$OUTF" 2> "$ERRF" if [ ! -s "$OUTF" ] && [ -s "$ERRF" ]; then echo "${green}PASS${reset} 18. an error goes to stderr, leaving stdout empty"; PASS=$((PASS+1)) else echo "${red}FAIL${reset} 18. error stream discipline (stdout: $(head -2 "$OUTF"))" FAIL=$((FAIL+1)) fi # Colour is emitted only to a terminal, so a redirected stream never carries # escape sequences -- into a document, a pipe, or a captured log. for pair in "ktext:--klammersets none -s hello -d" "kdesc:-k table" "kdiag:@i-x"; do cmd=${pair%%:*}; rest=${pair#*:} # shellcheck disable=SC2086 $cmd $rest > "$OUTF" 2> "$ERRF" if ! grep -q $'\033' "$OUTF" && ! grep -q $'\033' "$ERRF"; then echo "${green}PASS${reset} 19. no escape sequences from $cmd when redirected" PASS=$((PASS+1)) else echo "${red}FAIL${reset} 19. $cmd emitted colour to a non-terminal"; FAIL=$((FAIL+1)) fi done echo echo "-- a bare command is a request, not a failure --" # Usage is the result being asked for: stdout, exit 0. It exited 1, so # "kdesc && echo ok" reported failure for a successful help request. for cmd in ktext kdesc kdiag; do $cmd > "$OUTF" 2> "$ERRF"; status=$? if [ $status -eq 0 ] && [ -s "$OUTF" ] && ! [ -s "$ERRF" ]; then echo "${green}PASS${reset} 20. $cmd with no arguments: usage on stdout, exit 0" PASS=$((PASS+1)) else echo "${red}FAIL${reset} 20. $cmd bare: exit $status, stdout $(wc -l < "$OUTF") lines, stderr $(wc -l < "$ERRF") lines" FAIL=$((FAIL+1)) fi done echo echo "====================" echo "Results: ${PASS} passed, ${FAIL} failed" [ "$FAIL" -eq 0 ] || exit 1 exit 0