Files
klammertext/tst/kdiag_test.sh
Andy Kopra 240cff4278 An output policy for the three commands, and @cond as a true special form
A snapshot of the development tree.  The substantial changes since the last one:

COMMAND OUTPUT POLICY.  The three commands display text in exactly three cases,
and each owns a stream: LOGGING under "-v" greater than 0 and an ERROR before
termination go to STDERR; OUTPUT THE USER ASKED FOR goes to STDOUT.  For ktext
that output is a document, so "ktext doc.kt -d | ..." is now safe -- logging
used to share the stream and land inside the document.  A bare command prints
its usage and succeeds rather than failing.  Colour is emitted only to a
terminal, per stream, and NO_COLOR is honoured.

"-v 1" reports every decision whose outcome you could not have read off your own
input: the klammerset that was loaded and from which file, a font's directory, a
":files" name's file, how "-o" was expanded.  Higher levels are the trace.

The commands no longer warn and continue: an anomaly is an error, described with
its location.  Two exceptions remain, each for a stated reason -- a condition
that is expected and temporary by design, and a judgment that is a heuristic
rather than exact.

@cond IS NOW A TRUE SPECIAL FORM, resolved at APPLICATION time rather than when
the file is read.  Two consequences for a writer:

  * a state variable reaches the predicate.  "@@@state Flag :value true @@@
    @cond *Flag* | T | F @" renders "T"; it used to see the literal "*Flag*" and
    silently take the false branch.  The document now behaves like a klammer
    body, whose arguments are bound before its conditionals are decided.
  * nothing in a discarded branch happens -- it is not read, not evaluated, not
    expanded.  An @eval in the branch not taken used to run anyway.

Its predicate relation is total and strict: true, True, 1; false, False, 0, and
empty; anything else is an error at the @cond rather than silently false.

@eval REACHING OUTSIDE.  ":shell" and ":haskell" now keep the command's standard
error out of the document (it appears under "-v 1") and treat a nonzero exit as
an error naming what the command reported.  A command that exits nonzero on
purpose -- "grep" finding no match -- says so with "|| true".

KLAMMER SETS.  Several combine: "--klammersets a b c" loads all three in the
order given, sharing one namespace, with the definition modes deciding
collisions.  "none" means none and may not be combined with other symbols.  A
klammerset with symbol X is declared in a file X/X.k, which is what lets two
sets require the same third set without loading it twice.

TESTS.  Four new suites: the kdiag command's interface, the @eval primitive's
contract with the outside world, and verbosity at both tiers.  Three suites
that could not run on macOS at all now do.

Assembled from dev commit 6c8ee6c22fca.
2026-08-16 01:37:59 +02:00

312 lines
15 KiB
Bash
Executable File

#!/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