Files
klammertext/tst/klammerset_test.sh
Andy Kopra ef77f03584 Klammerset: the @@@klammerset construct, its search path, and const correctness
The @@@klammerset system command formally declares a klammerset — a
named, logically related group of klammer definitions — with an
operative, idempotent declaration (:requires and :files load in order
at the declaration point, relative to the declaring file). A bare
symbol given to ktext -k, kdesc --input, or :requires resolves to
x/x.k on the search path: the document's directory, then
KLAMMERTEXT_KLAMMERSETS, then KLAMMERTEXT_HOME; kdesc --klammerset
lists the available sets. sks/sks.k is the first declared klammerset,
so `-k sks` loads the SKS by name. The engine's lookup classes were
renamed *_set → *_registry to keep the two concepts apart, and the
whole C++ tree now follows standard const-correctness conventions.
tst/ gains klammerset_test.sh (18 cases).

(from dev 64b1abf23e56)
2026-07-30 23:50:07 +02:00

285 lines
10 KiB
Bash
Executable File

#!/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 -> <dir>/<symbol>/<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 <symbol>/<symbol>.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 ]