312 lines
15 KiB
Bash
312 lines
15 KiB
Bash
|
|
#!/bin/bash
|
||
|
|
#
|
||
|
|
# kdiag_test.sh — the kdiag command's interface.
|
||
|
|
#
|
||
|
|
# kdiag is the PROGRAMMER's command in the 2026-08-14 three-command redesign:
|
||
|
|
# ktext renders a document for an author, kdesc describes a klammer set for a
|
||
|
|
# designer, kdiag dissects katoms and the Machine. It had no suite of its own,
|
||
|
|
# which is how four defects survived in it -- including a "--check" that
|
||
|
|
# reported "0 diagnostics, 0 errors" for every input there is, while every
|
||
|
|
# other suite stayed green. An absent test is what let that read as a pass, so
|
||
|
|
# each of the four is pinned here by OUTCOME.
|
||
|
|
#
|
||
|
|
# Its peculiarities, and why each is what it is:
|
||
|
|
#
|
||
|
|
# * Its input positional is OPTIONAL, so "kdiag --machine" answers a question
|
||
|
|
# about the Machine rather than about a document.
|
||
|
|
# * --klammer registers the @@ tier and --system the @@@ tier; --process does
|
||
|
|
# both. A registered definition's katoms are CONSUMED, so they show up
|
||
|
|
# only under --replaced -- their disappearance is the evidence.
|
||
|
|
# * Extraction is TOLERANT here and nowhere else. The tiers are ordered (a
|
||
|
|
# klammer names a target), so running one without the other leaves
|
||
|
|
# definitions that cannot be registered. In kdiag that is not an error:
|
||
|
|
# the definition is skipped and its katoms stay visible, which IS the
|
||
|
|
# report. ktext must keep throwing on the same input, and the contrast is
|
||
|
|
# asserted, not assumed.
|
||
|
|
# * It deliberately has NO --klammersets. A klammerset arrives the way
|
||
|
|
# anything else does, "@read sks/sks.k @": a debugger must not depend on
|
||
|
|
# the machinery it debugs, so kdiag still runs when symbol resolution is
|
||
|
|
# what broke.
|
||
|
|
#
|
||
|
|
# Engine tier: no klammerset is loaded, so every fixture defines what it needs
|
||
|
|
# inline. The one case that wants the SKS reads it by path.
|
||
|
|
#
|
||
|
|
# Usage: ./kdiag_test.sh (needs KLAMMERTEXT_HOME set; kdiag and ktext on PATH)
|
||
|
|
# Exit code: 0 if all tests pass, 1 otherwise.
|
||
|
|
|
||
|
|
PASS=0
|
||
|
|
FAIL=0
|
||
|
|
KDIAG=kdiag
|
||
|
|
KTEXT=ktext
|
||
|
|
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
|
||
|
|
|
||
|
|
red=$'\033[31m'
|
||
|
|
green=$'\033[32m'
|
||
|
|
bold=$'\033[1m'
|
||
|
|
reset=$'\033[0m'
|
||
|
|
|
||
|
|
plain() { printf '%s' "$1" | sed 's/\x1b\[[0-9;]*m//g'; }
|
||
|
|
|
||
|
|
# GNU coreutils' "timeout" is NOT present on macOS, and Homebrew's is named
|
||
|
|
# "gtimeout", so a bare "timeout" made this suite fail WHOLESALE there -- every
|
||
|
|
# case, because the command never ran at all (found on Olion, 2026-08-16). The
|
||
|
|
# guard is a safety net against a hung command, not part of what is being
|
||
|
|
# tested, so it is optional: bound the command where the tool exists, run it
|
||
|
|
# directly where it does not.
|
||
|
|
if command -v timeout >/dev/null 2>&1; then
|
||
|
|
limited() { timeout 60 "$@"; }
|
||
|
|
elif command -v gtimeout >/dev/null 2>&1; then
|
||
|
|
limited() { gtimeout 60 "$@"; }
|
||
|
|
else
|
||
|
|
limited() { "$@"; }
|
||
|
|
fi
|
||
|
|
|
||
|
|
# run KDIAG_ARGS... — sets OUT (colour codes stripped) and STATUS. The status
|
||
|
|
# must be taken from the command itself: a pipeline inside a command
|
||
|
|
# substitution reports the SED's status, so "kdiag | plain" would call every
|
||
|
|
# failure a success. Strip afterwards instead.
|
||
|
|
OUT=''
|
||
|
|
STATUS=0
|
||
|
|
run() {
|
||
|
|
OUT=$(limited "$@" 2>&1); STATUS=$?
|
||
|
|
OUT=$(plain "$OUT")
|
||
|
|
}
|
||
|
|
|
||
|
|
# A signal death is not an error exit: 128+n. Report it distinctly, so a
|
||
|
|
# crash can never be read as a reported error.
|
||
|
|
died_by_signal() { [ "$1" -gt 128 ]; }
|
||
|
|
|
||
|
|
pass() { echo "${green}PASS${reset} $1"; PASS=$((PASS + 1)); }
|
||
|
|
fail() { echo "${red}FAIL${reset} $1"; [ -n "$2" ] && echo " $2"; FAIL=$((FAIL + 1)); }
|
||
|
|
|
||
|
|
# shows NAME PATTERN KDIAG_ARGS... — exits 0 and the output matches PATTERN.
|
||
|
|
shows() {
|
||
|
|
local name="$1" pattern="$2"; shift 2
|
||
|
|
run "$KDIAG" "$@"
|
||
|
|
if died_by_signal "$STATUS"; then
|
||
|
|
fail "$name" "died by signal $((STATUS - 128))"; return
|
||
|
|
fi
|
||
|
|
if [ "$STATUS" -ne 0 ]; then
|
||
|
|
fail "$name" "exit $STATUS: $(printf '%s' "$OUT" | head -2)"; return
|
||
|
|
fi
|
||
|
|
if printf '%s' "$OUT" | grep -Eq -- "$pattern"; then
|
||
|
|
pass "$name"
|
||
|
|
else
|
||
|
|
fail "$name" "no match for [$pattern]; got: $(printf '%s' "$OUT" | head -2)"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
# absent NAME PATTERN KDIAG_ARGS... — exits 0 and the output does NOT match.
|
||
|
|
absent() {
|
||
|
|
local name="$1" pattern="$2"; shift 2
|
||
|
|
run "$KDIAG" "$@"
|
||
|
|
if died_by_signal "$STATUS"; then
|
||
|
|
fail "$name" "died by signal $((STATUS - 128))"; return
|
||
|
|
fi
|
||
|
|
if [ "$STATUS" -ne 0 ]; then
|
||
|
|
fail "$name" "exit $STATUS: $(printf '%s' "$OUT" | head -2)"; return
|
||
|
|
fi
|
||
|
|
if printf '%s' "$OUT" | grep -Eq -- "$pattern"; then
|
||
|
|
fail "$name" "unexpected match [$pattern]"
|
||
|
|
else
|
||
|
|
pass "$name"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
# reports NAME PATTERN KDIAG_ARGS... — NONZERO exit, no signal, PATTERN shown.
|
||
|
|
reports() {
|
||
|
|
local name="$1" pattern="$2"; shift 2
|
||
|
|
run "$KDIAG" "$@"
|
||
|
|
if died_by_signal "$STATUS"; then
|
||
|
|
fail "$name" "died by signal $((STATUS - 128))"; return
|
||
|
|
fi
|
||
|
|
if [ "$STATUS" -eq 0 ]; then
|
||
|
|
fail "$name" "expected a nonzero exit, got 0"; return
|
||
|
|
fi
|
||
|
|
if printf '%s' "$OUT" | grep -Eq -- "$pattern"; then
|
||
|
|
pass "$name"
|
||
|
|
else
|
||
|
|
fail "$name" "no match for [$pattern]; got: $(printf '%s' "$OUT" | head -2)"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
# A klammer definition and an application of it, with no target named: the
|
||
|
|
# general target needs no @@@target, so the @@ tier can be exercised alone.
|
||
|
|
GREET='@@greet name : Hello, *name*. @@ @greet World @'
|
||
|
|
|
||
|
|
# Input that declares its own target AND a klammer for it. The two tiers are
|
||
|
|
# ordered, so this is what tells --klammer and --system apart.
|
||
|
|
OWNTARGET='@@@target foo | Foo output @@@ @@bar.foo : B @@ text'
|
||
|
|
|
||
|
|
# A file for --read to pull in.
|
||
|
|
READFIXTURE=$(mktemp /tmp/kdiag_read.XXXXXX)
|
||
|
|
printf 'READFILE\n' > "$READFIXTURE"
|
||
|
|
trap 'rm -f "$READFIXTURE"' EXIT
|
||
|
|
|
||
|
|
echo "${bold}kdiag interface tests${reset}"
|
||
|
|
echo "====================="
|
||
|
|
echo
|
||
|
|
|
||
|
|
echo "-- the input positional is optional --"
|
||
|
|
# The Machine's initial state is a question about the Machine, and the one
|
||
|
|
# thing a programmer wants before feeding it anything.
|
||
|
|
shows " 1. --machine with no input at all" 'Machine' --machine
|
||
|
|
shows " 2. ... and shows the built-in argtypes" 'Argtypes \([0-9]+\)' --machine
|
||
|
|
shows " 3. ... and the pseudo-targets" 'Targets \([0-9]+\)' --machine
|
||
|
|
shows " 4. an input is still accepted" '⟨@i' --type '@i x @'
|
||
|
|
# Listed under "Arguments:", so it must not be repeated under "Options:".
|
||
|
|
usage=$(plain "$(limited "$KDIAG" 2>&1)")
|
||
|
|
n_input=$(printf '%s\n' "$usage" | grep -c '\[<input>\]')
|
||
|
|
if [ "$n_input" = "2" ]; then
|
||
|
|
pass " 5. usage lists [<input>] once as a positional (plus the Usage: line)"
|
||
|
|
else
|
||
|
|
fail " 5. usage lists [<input>] $n_input times, expected 2" \
|
||
|
|
"$(printf '%s\n' "$usage" | grep -n '\[<input>\]')"
|
||
|
|
fi
|
||
|
|
|
||
|
|
echo
|
||
|
|
echo "-- registration: one flag per definition tier --"
|
||
|
|
# A registered definition is CONSUMED. Its disappearance from the katom
|
||
|
|
# display is the evidence that it registered, and --replaced brings it back.
|
||
|
|
absent " 6. --klammer consumes the @@ definition" '⟨@@greet⟩' --klammer "$GREET"
|
||
|
|
shows " 7. ... and --replaced shows it again" '⟨@@greet⟩' --klammer --replaced "$GREET"
|
||
|
|
shows " 8. ... and the klammer is in the registry" 'Klammers \(1\)' --klammer --machine "$GREET"
|
||
|
|
absent " 9. --system consumes the @@@ definition" '⟨@@@target⟩' --system "$OWNTARGET"
|
||
|
|
shows "10. ... and the target is in the registry" 'foo → ' --system --machine "$OWNTARGET"
|
||
|
|
shows "11. --process registers both tiers" 'Klammers \(1\)' --process --machine "$OWNTARGET"
|
||
|
|
absent "12. ... and consumes both" '⟨@@' --process "$OWNTARGET"
|
||
|
|
|
||
|
|
echo
|
||
|
|
echo "-- tolerant extraction: kdiag only --"
|
||
|
|
# --klammer alone cannot register @@bar.foo, because the target "foo" it names
|
||
|
|
# is declared by the @@@ tier this run did not process. Skipping it is the
|
||
|
|
# report, not a failure -- the definition's katoms stay on screen.
|
||
|
|
shows "13. --klammer alone does not fail on an unregisterable klammer" \
|
||
|
|
'⟨@@bar.foo⟩' --klammer "$OWNTARGET"
|
||
|
|
absent "14. ... and does not register it either" \
|
||
|
|
'Klammers \(1\)' --klammer --machine "$OWNTARGET"
|
||
|
|
shows "15. --system alone leaves the @@ katoms visible" \
|
||
|
|
'⟨@@bar.foo⟩' --system "$OWNTARGET"
|
||
|
|
# The contrast. In ktext the same input is a genuine error: a document naming
|
||
|
|
# an undefined target would otherwise render wrongly and silently.
|
||
|
|
# The pattern must sit on ONE line of the message: an error is wrapped for the
|
||
|
|
# terminal, so "is not defined" can arrive with a newline inside it and a
|
||
|
|
# line-oriented grep will never see it.
|
||
|
|
run "$KTEXT" --klammersets none -s '@@bar.nosuchtarget : B @@' -d
|
||
|
|
if [ "$STATUS" -ne 0 ] && printf '%s' "$OUT" | grep -q 'target "nosuchtarget"'; then
|
||
|
|
pass "16. ktext still THROWS on a klammer naming an undefined target"
|
||
|
|
else
|
||
|
|
fail "16. ktext accepted an undefined target (exit $STATUS)" \
|
||
|
|
"$(printf '%s' "$OUT" | head -2)"
|
||
|
|
fi
|
||
|
|
|
||
|
|
echo
|
||
|
|
echo "-- --check --"
|
||
|
|
# The regression: check_machine() read machine.m_katoms, which read() fills and
|
||
|
|
# process() -- how kdiag builds its katoms -- does not. So it had nothing to
|
||
|
|
# check and said so, for every input there is. These two are the guard: a
|
||
|
|
# clean check must be able to fail.
|
||
|
|
shows "17. a correct input checks clean" '0 diagnostics' --process --check "$GREET"
|
||
|
|
reports "18. an undefined klammer is found" '@nosuch is not defined' \
|
||
|
|
--process --check '@@g : @nosuch x @ @@'
|
||
|
|
reports "19. ... and the body it sits in is named" 'in body of @g' \
|
||
|
|
--process --check '@@g : @nosuch x @ @@'
|
||
|
|
reports "20. a wrong argument count is found" 'is given 2' \
|
||
|
|
--process --check "@@greet name : Hello, *name*. @@ @greet a | b @"
|
||
|
|
# Without --klammer/--system/--process nothing is registered, so every
|
||
|
|
# application is undefined. Reporting 80 spurious errors without saying why
|
||
|
|
# would be worse than the old silence; the hint is part of the report.
|
||
|
|
reports "21. --check alone explains why nothing is registered" \
|
||
|
|
'No klammers are registered' --check "$GREET"
|
||
|
|
reports "22. ... and names the way to fix it" '--process' --check "$GREET"
|
||
|
|
# --machine must print even when the check failed: the state shown last has to
|
||
|
|
# reflect everything that happened, so the nonzero exit waits for it.
|
||
|
|
reports "23. --machine still prints after a failed check" \
|
||
|
|
'Machine' --process --check --machine '@nosuch x @'
|
||
|
|
|
||
|
|
echo
|
||
|
|
echo "-- a klammerset reaches kdiag only as input --"
|
||
|
|
# No --klammersets, deliberately: a debugger must not depend on the machinery
|
||
|
|
# it debugs. The flag must be REJECTED, not silently ignored.
|
||
|
|
reports "24. --klammersets is not a kdiag argument" 'argument error' \
|
||
|
|
--klammersets sks '@i x @'
|
||
|
|
# ...so the advice on an undefined-klammer error must not offer it. It did
|
||
|
|
# until 2026-08-15: the one remedy the error suggested was a flag this command
|
||
|
|
# rejects. A wrong hint is worse than none, because it is followed.
|
||
|
|
#
|
||
|
|
# The trigger is a GENERATOR: an @eval whose result holds a klammer. A klammer
|
||
|
|
# written literally in the input is only displayed, never applied, so nothing
|
||
|
|
# reaches the catch block -- which is also why this advice went years unread.
|
||
|
|
UNDEF='@eval chr(64)+"nosuch 7 "+chr(64) @'
|
||
|
|
reports "24a. the error advice names a way kdiag has" '@read sks/sks\.k @' \
|
||
|
|
--process "$UNDEF"
|
||
|
|
# absent_in_error NAME PATTERN ARGS... -- the pattern is missing whatever the
|
||
|
|
# exit status; these run on a path that exits 1 by design.
|
||
|
|
absent_in_error() {
|
||
|
|
local name="$1" pattern="$2"; shift 2
|
||
|
|
run "$KDIAG" "$@"
|
||
|
|
if printf '%s' "$OUT" | grep -Eq -- "$pattern"; then
|
||
|
|
fail "$name" "unexpected match [$pattern]"
|
||
|
|
else
|
||
|
|
pass "$name"
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
absent_in_error "24b. ... and not the flag it rejects" '\-\-klammersets' \
|
||
|
|
--process "$UNDEF"
|
||
|
|
# The advice is appended to the description, which is then justified to 80
|
||
|
|
# columns -- without a blank line between them the two ran together into one
|
||
|
|
# word ("...unspecified targetkdiag loads no klammerset...").
|
||
|
|
absent_in_error "24c. ... and does not run onto the message" 'target[a-z]' \
|
||
|
|
--process "$UNDEF"
|
||
|
|
shows "25. @read loads the SKS instead" 'Klammers \([0-9][0-9]+\)' \
|
||
|
|
"@read $K/sks/sks.k @" --process --machine
|
||
|
|
shows "26. ... and the SKS then checks clean" '0 diagnostics' \
|
||
|
|
"@read $K/sks/sks.k @" --process --check
|
||
|
|
# ...and the clean result above is a real one: the same input with a fault
|
||
|
|
# added must still be caught, or case 26 says nothing.
|
||
|
|
reports "27. ... while a fault added to it is still caught" '@nosuchklammer is not defined' \
|
||
|
|
"@read $K/sks/sks.k @ @@bad : @nosuchklammer x @ @@" --process --check
|
||
|
|
|
||
|
|
echo
|
||
|
|
echo "-- the katom display flags --"
|
||
|
|
shows "28. --type subscripts the katom type" '⟨@i.⟩' --type '@i x @'
|
||
|
|
shows "29. --index subscripts the list index" '⟨@i.' --index '@i x @'
|
||
|
|
shows "30. --spans shows span endpoints" '⟨@i.⟩.*⟨@.⟩' --spans '@i x @'
|
||
|
|
shows "31. --args parses positional arguments" 'required' --args --pos 2 'a | b | c'
|
||
|
|
shows "32. --pos sets how many are positional" 'rest: c' --args --pos 2 'a | b | c'
|
||
|
|
shows "33. --ignore removes removed text" '^a b' --ignore 'a #[gone]# b'
|
||
|
|
shows "34. --all shows what was removed" '⟨gone⟩' --ignore --all 'a #[gone]# b'
|
||
|
|
shows "35. --ws applies whitespace operators" 'ab' --ws 'a#- b'
|
||
|
|
# A DECOMPOSED sequence: the base letter followed by U+0308 COMBINING
|
||
|
|
# DIAERESIS, not the precomposed U+00FC. The diacritic FOLLOWS the base
|
||
|
|
# letter -- that is the design of the ^ codes -- so the two spellings look
|
||
|
|
# identical in an editor and only one of them matches.
|
||
|
|
shows "36. --nonascii decodes to a combining sequence" 'ü' --nonascii '^u"'
|
||
|
|
shows "37. --literal marks a literal span" 'x' --literal "^'x'^"
|
||
|
|
# Evaluating needs a target: Eval::eval re-reads its result under K_target, and
|
||
|
|
# kdiag has no target argument, so with the variable unset EVERY @eval died
|
||
|
|
# with an argument error naming something the user never wrote. A command that
|
||
|
|
# specifies no target evaluates under the GENERAL target (Andy, 2026-08-15),
|
||
|
|
# which is also what kdiag means -- it loads no klammerset, so nothing
|
||
|
|
# target-specific is in scope. All three eval modes are checked, because the
|
||
|
|
# failure was in the shared read-back and not in any one of them.
|
||
|
|
shows "38. --eval evaluates a Python expression" '^4$' --eval '@eval 2 + 2 @'
|
||
|
|
shows "38a. ... and --process does too" '^4$' --process '@eval 2 + 2 @'
|
||
|
|
shows "38b. ... in :shell mode" '^hi$' --process '@eval :shell echo hi @'
|
||
|
|
shows "38c. ... and a module.function auto-imports" '^3$' --process '@eval len("abc") @'
|
||
|
|
shows "38d. --read reads a file" 'READFILE' --read "@read $READFIXTURE @"
|
||
|
|
shows "39. --cond selects a branch" 'yes' --cond '@cond true | yes | no @'
|
||
|
|
shows "40. --rewrite reports a rewrite" '.' --rewrite '@i x @'
|
||
|
|
shows "41. --text shows text katoms" 'x' --text '@i x @'
|
||
|
|
shows "42. --ignored shows ignored katoms" '.' --ignore --ignored 'a #[gone]# b'
|
||
|
|
|
||
|
|
echo
|
||
|
|
echo "-- verbosity is about processing --"
|
||
|
|
shows "43. -v takes a value" '.' -v 1 '@i x @'
|
||
|
|
|
||
|
|
echo
|
||
|
|
echo "====================="
|
||
|
|
echo "Results: ${PASS} passed, ${FAIL} failed"
|
||
|
|
[ "$FAIL" -eq 0 ] || exit 1
|
||
|
|
exit 0
|