#!/bin/bash # # klammerset_test.sh — Regression tests for the @@@klammerset system command. # # @@@klammerset declares a Klammerset: a named, logically related group of # klammer definitions. The declaration is operative — processing it reads # the :requires files and then the :files, in list order, at the point of the # declaration; relative names resolve against the declaring file's directory, # never the cwd. A repeated declaration of an already-registered symbol is # skipped (loaded once), which is what makes :requires idempotent. The # klammers themselves live in the Machine's flat Klammer_registry; the # Klammerset holds metadata and the file list only. # # Engine tier: no SKS. Fixtures live in tst/klammerset/ and define their own # target ("fix") inline. # # Usage: ./klammerset_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} FIX=$K/tst/klammerset red=$'\033[31m' green=$'\033[32m' bold=$'\033[1m' reset=$'\033[0m' # strip leading/trailing blank lines and surrounding whitespace trim() { awk '{ sub(/[ \t\r]+$/, "") } { line[NR]=$0 } END { f=1; while (f<=NR && line[f]=="") f++; l=NR; while (l>=1 && line[l]=="") l--; for (i=f;i<=l;i++) print line[i] }'; } # check_eq TEST_NAME EXPECTED KTEXT_ARGS... # Runs ktext, expects exit status 0, and compares trimmed stdout to EXPECTED. check_eq() { local test_name="$1" local expected="$2" shift 2 local output status output=$("$KTEXT" "$@" 2>/tmp/klammerset_test_err.$$) status=$? output=$(printf '%s' "$output" | trim) if [ $status -ne 0 ]; then echo "${red}FAIL${reset} $test_name — ktext exited $status" echo " stderr: $(head -3 /tmp/klammerset_test_err.$$)" 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 } # check_contains TEST_NAME SUBSTRING KTEXT_ARGS... # Runs ktext, expects exit status 0, and checks that the combined output # contains SUBSTRING. check_contains() { local test_name="$1" local needle="$2" shift 2 local output status output=$("$KTEXT" "$@" 2>&1) status=$? if [ $status -ne 0 ]; then echo "${red}FAIL${reset} $test_name — ktext exited $status" echo " output: $(echo "$output" | head -3)" FAIL=$((FAIL + 1)) return fi if printf '%s' "$output" | grep -qF "$needle"; then echo "${green}PASS${reset} $test_name" PASS=$((PASS + 1)) else echo "${red}FAIL${reset} $test_name" echo " expected output to contain: [$needle]" echo " got: $(echo "$output" | head -5)" FAIL=$((FAIL + 1)) fi } # check_fails TEST_NAME SUBSTRING KTEXT_ARGS... # Runs ktext, expects a NONZERO exit status, and checks that the combined # output contains SUBSTRING. check_fails() { local test_name="$1" local needle="$2" shift 2 local output status output=$("$KTEXT" "$@" 2>&1) status=$? if [ $status -eq 0 ]; then echo "${red}FAIL${reset} $test_name — expected an error, ktext exited 0" FAIL=$((FAIL + 1)) return fi if printf '%s' "$output" | grep -qF "$needle"; then echo "${green}PASS${reset} $test_name" PASS=$((PASS + 1)) else echo "${red}FAIL${reset} $test_name" echo " expected error output to contain: [$needle]" echo " got: $(echo "$output" | head -5)" FAIL=$((FAIL + 1)) fi } echo "${bold}@@@klammerset tests${reset}" echo "=======================" echo # --- Loading --------------------------------------------------------------- # 1. :files load in list order: klammers.k defines @greet for the target # that base.k declares, so base.k must have been read first. check_eq "1. :files load in order (target before klammer)" \ "Hello World" \ -k "$FIX/decl.k" -s '@greet World @' -t fix # 2. :requires loads the dependency before the set's own files. check_eq "2. :requires loads the dependency" \ "--" \ -k "$FIX/decl.k" -s '@dash@' -t fix # 3. Program order: a definition AFTER the declaration in the declaring file # is available (there is no :text argument; the declaring file's own # content plays that role). check_eq "3. trailing definition in the declaring file" \ "AFTER" \ -k "$FIX/decl.k" -s '@after@' -t fix # 4. Relative :files names resolve against the DECLARING file's directory, # not the cwd (run from an unrelated directory). output=$( (cd /tmp && "$KTEXT" -k "$FIX/decl.k" -s '@greet Elsewhere @' -t fix) 2>/dev/null | trim ) if [ "$output" = "Hello Elsewhere" ]; then echo "${green}PASS${reset} 4. :files resolve against the declaring file's directory" PASS=$((PASS + 1)) else echo "${red}FAIL${reset} 4. :files resolve against the declaring file's directory" echo " expected: [Hello Elsewhere]" echo " got: [$output]" FAIL=$((FAIL + 1)) fi # 5. A filename with a space in the :files list (standalone "/" separator). check_eq "5. spacey filename in :files" \ "SPACEY" \ -k "$FIX/spacey.k" -s '@spacey@' -t fix # --- The already-loaded guard ------------------------------------------------ # 6. A second declaration of an already-registered symbol is skipped, not an # error: wrapper.k requires decl.k (registers "kit") and then decl2.k # (re-declares "kit"); the wrapper still loads and kit's klammers work. check_eq "6. duplicate declaration is skipped, not an error" \ "Hello Again" \ -k "$FIX/wrapper.k" -s '@greet Again @' -t fix # 7. ...and the skipped declaration's files are NOT loaded. check_fails "7. skipped declaration loads none of its files" \ "only_dup" \ -k "$FIX/wrapper.k" -s '@only_dup@' -t fix # --- Introspection ----------------------------------------------------------- # 8. -m lists the registered klammersets. check_contains "8. -m shows the klammerset symbol" \ "kit" \ -k "$FIX/decl.k" -s 'x' -t fix -m check_contains "9. -m shows the required klammerset too" \ "Utility klammers for engine tests" \ -k "$FIX/decl.k" -s 'x' -t fix -m # --- Errors -------------------------------------------------------------------- # 10. A listed file that does not exist is a clean klammerset error. check_fails "10. missing file in :files" \ "missing.k" \ -k "$FIX/bad_file.k" -s 'x' -t fix # 11. A symbol must be an identifier (starts with a letter; letters, digits, # underscores). check_fails "11. invalid symbol rejected" \ "not valid" \ -k none -s '@@@klammerset 9bad | Bad symbol @@@' -d # --- The search path (symbol -> //.k) ------------------- # Runtime fixtures: a document directory holding a local klammerset, and a # separate directory serving as a KLAMMERTEXT_KLAMMERSETS stage. DOCDIR=$(mktemp -d /tmp/klammerset_doc.XXXXXX) ENVDIR=$(mktemp -d /tmp/klammerset_env.XXXXXX) trap 'rm -rf "$DOCDIR" "$ENVDIR" /tmp/klammerset_test_err.$$' EXIT mkdir -p "$DOCDIR/locset" "$ENVDIR/envset" "$ENVDIR/locset" printf '@@@klammerset locset | Document-local set @@@\n@@@target fixL | Fixture @@@\n@@local_k.fixL : LOCAL @@\n' \ > "$DOCDIR/locset/locset.k" printf '@local_k@\n' > "$DOCDIR/doc.kt" printf '@@@klammerset envset | Installed set @@@\n@@@target fixE | Fixture @@@\n@@env_k.fixE : ENV @@\n' \ > "$ENVDIR/envset/envset.k" printf '@@@klammerset locset | Shadow candidate @@@\n@@@target fixL | Fixture @@@\n@@local_k.fixL : ENV-SHADOWED @@\n' \ > "$ENVDIR/locset/locset.k" # 12. Stage 1 for a file input: the document's directory. check_eq "12. symbol resolves in the document's directory" \ "LOCAL" \ "$DOCDIR/doc.kt" -k locset -t fixL -d # 13. Stage 1 for string input: the cwd stands in for the document. output=$( (cd "$DOCDIR" && "$KTEXT" -s '@local_k@' -k locset -t fixL) 2>/dev/null | trim ) if [ "$output" = "LOCAL" ]; then echo "${green}PASS${reset} 13. cwd stands in for the document (string input)" PASS=$((PASS + 1)) else echo "${red}FAIL${reset} 13. cwd stands in for the document (string input)" echo " expected: [LOCAL] got: [$output]" FAIL=$((FAIL + 1)) fi # 14. Stage 2: the KLAMMERTEXT_KLAMMERSETS directories. output=$(KLAMMERTEXT_KLAMMERSETS=$ENVDIR "$KTEXT" -s '@env_k@' -k envset -t fixE 2>/dev/null | trim) if [ "$output" = "ENV" ]; then echo "${green}PASS${reset} 14. symbol resolves in KLAMMERTEXT_KLAMMERSETS" PASS=$((PASS + 1)) else echo "${red}FAIL${reset} 14. symbol resolves in KLAMMERTEXT_KLAMMERSETS" echo " expected: [ENV] got: [$output]" FAIL=$((FAIL + 1)) fi # 15. Shadowing: the document-local set wins over the installed one. output=$(KLAMMERTEXT_KLAMMERSETS=$ENVDIR "$KTEXT" "$DOCDIR/doc.kt" -k locset -t fixL -d 2>/dev/null | trim) if [ "$output" = "LOCAL" ]; then echo "${green}PASS${reset} 15. document-local set shadows the installed one" PASS=$((PASS + 1)) else echo "${red}FAIL${reset} 15. document-local set shadows the installed one" echo " expected: [LOCAL] got: [$output]" FAIL=$((FAIL + 1)) fi # 16. :requires by symbol, resolved with the declaring directory as the # local stage (the required set sits inside the declaring set's dir). mkdir -p "$DOCDIR/kit2/locset" cp "$DOCDIR/locset/locset.k" "$DOCDIR/kit2/locset/locset.k" printf '@@@klammerset kit2 | Requires by symbol :requires locset @@@\n' > "$DOCDIR/kit2/kit2.k" check_eq "16. :requires accepts a symbol (declaring-dir stage)" \ "LOCAL" \ -k "$DOCDIR/kit2/kit2.k" -s '@local_k@' -t fixL # 17. Stage 3: $KLAMMERTEXT_HOME — the kdesc listing enumerates sks # (sks/sks.k already satisfies the /.k convention). output=$(kdesc --klammerset 2>/dev/null) if printf '%s' "$output" | grep -q 'sks/sks\.k'; then echo "${green}PASS${reset} 17. kdesc --klammerset lists sks from KLAMMERTEXT_HOME" PASS=$((PASS + 1)) else echo "${red}FAIL${reset} 17. kdesc --klammerset lists sks from KLAMMERTEXT_HOME" echo " got: $(echo "$output" | head -3)" FAIL=$((FAIL + 1)) fi # 18. An unknown symbol is a clean error naming the search directories. check_fails "18. unknown symbol names the search path" \ "was not found" \ -s 'x' -k nosuchset -d echo echo "=======================" echo "Results: ${green}$PASS passed${reset}, $([ $FAIL -gt 0 ] && echo "${red}$FAIL failed${reset}" || echo "0 failed")" [ $FAIL -eq 0 ]