#!/bin/bash # # recursion_test.sh — The klammer application recursion guard. # # Before the guard, a klammer that applied itself -- directly or through a # cycle -- descended until the C++ stack was exhausted. The process died with # SIGSEGV: no message, no location, no indication of which klammer was at # fault, and a core dump. For a language whose premise is that users define # their own klammers, that was the worst available failure mode. # # Machine::apply_klammer() now carries a depth guard (Depth_guard in # mac/machine.cpp) that raises a Recursion_error naming the klammer and its # location. Separately, the top-level fixed-point loop in Machine::apply() # ends when a pass applies no klammer -- rather than when the katom list stops # growing -- and exceeding its round limit is an error rather than a message # followed by rendering a document with live klammers still in it. # # These are engine tests: no klammer set is loaded (-k none) and every klammer # used is defined inline as a fixture. # # Usage: ./recursion_test.sh # Exit code: 0 if all tests pass, 1 otherwise. PASS=0 FAIL=0 KTEXT=ktext K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set} red=$'\033[31m' green=$'\033[32m' bold=$'\033[1m' reset=$'\033[0m' # check_error TEST_NAME PATTERN KTEXT_ARGS... # Runs ktext, expects a NONZERO exit status and PATTERN in the message. # A signal death (exit >= 128) is called out separately: that is the exact # regression this suite exists to prevent, and reporting it as "some error" # would hide it. check_error() { local test_name="$1" local pattern="$2" shift 2 local output status output=$("$KTEXT" "$@" 2>&1) status=$? if [ $status -ge 128 ]; then echo "${red}FAIL${reset} $test_name — ktext died from signal $((status - 128))" FAIL=$((FAIL + 1)) return fi if [ $status -eq 0 ]; then echo "${red}FAIL${reset} $test_name — expected an error but ktext succeeded" FAIL=$((FAIL + 1)) return fi if echo "$output" | grep -qF "$pattern"; then echo "${green}PASS${reset} $test_name" PASS=$((PASS + 1)) else echo "${red}FAIL${reset} $test_name — expected error to contain [$pattern]" echo " output: $(echo "$output" | head -4)" FAIL=$((FAIL + 1)) fi } # check_eq TEST_NAME EXPECTED KTEXT_ARGS... check_eq() { local test_name="$1" local expected="$2" shift 2 local output status output=$("$KTEXT" "$@" 2>/dev/null) status=$? output=$(printf '%s' "$output" | tr -d '\n' | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//') if [ $status -ne 0 ]; then echo "${red}FAIL${reset} $test_name — ktext exited $status" FAIL=$((FAIL + 1)) return fi if [ "$output" = "$expected" ]; then echo "${green}PASS${reset} $test_name" PASS=$((PASS + 1)) else echo "${red}FAIL${reset} $test_name" echo " expected: [$expected]" echo " got: [$output]" FAIL=$((FAIL + 1)) fi } echo "${bold}Klammer recursion guard tests${reset}" echo "=============================" echo # --- Non-termination is an error, not a crash --- check_error " 1. direct self-recursion is caught" \ "does not terminate" \ -k none -s '@@f : x @f@ @@ @f@' -d check_error " 2. the offending klammer is named" \ 'applying "f"' \ -k none -s '@@f : x @f@ @@ @f@' -d check_error " 3. mutual recursion is caught" \ "does not terminate" \ -k none -s '@@a : ( @b@ ) @@ @@b : [ @a@ ] @@ @a@' -d check_error " 4. self-recursion through an argument is caught" \ "does not terminate" \ -k none -s '@@w t : < *t* > @@ @@r : @w @r@ @ @@ @r@' -d # --- Terminating nesting is untouched --- check_eq " 5. deep but finite nesting still reduces" \ "<<<<>>>>" \ -k none -s '@@w t : <*t*> @@ @w @w @w @w @w x @ @ @ @ @' -d check_eq " 6. a chain of klammers generating klammers reduces" \ "END" \ -k none -s '@@k1 : @k2@ @@ @@k2 : @k3@ @@ @@k3 : @k4@ @@ @@k4 : @k5@ @@ @@k5 : @k6@ @@ @@k6 : @k7@ @@ @@k7 : @k8@ @@ @@k8 : END @@ @k1@' -d # --- The fixed point ends on "nothing was applied", not "nothing was added" --- # # A klammer whose body is empty reduces without adding katoms. Under the old # size-comparison test such a klammer looked like no progress at all. check_eq " 7. a klammer with an empty body reduces" \ "a b" \ -k none -s '@@nothing : @@ a @nothing@ b' -d echo echo "=============================" echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}" [ $FAIL -eq 0 ]