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.
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
.PHONY: test
|
||||
test:
|
||||
./cond_test.sh
|
||||
./eval_test.sh
|
||||
./recursion_test.sh
|
||||
./check_test.sh
|
||||
./deftype_test.sh
|
||||
@@ -22,5 +23,7 @@ test:
|
||||
./target_list_test.sh
|
||||
./coverage_test.sh
|
||||
./command_option_test.sh
|
||||
./verbosity_test.sh
|
||||
./kdesc_test.sh
|
||||
./kdiag_test.sh
|
||||
./editor_test.sh
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
# would take "some words" as the value and the alone value would never be
|
||||
# reached. That is a definition-time error.
|
||||
#
|
||||
# Engine tier: these tests run with -k none and define their own argtypes
|
||||
# Engine tier: these tests run with --klammersets none and define their own argtypes
|
||||
# and klammers inline, so they do not depend on the Standard Klammer Set.
|
||||
#
|
||||
# Usage: ./alone_test.sh (LSan suppressions come from env/runtime.env)
|
||||
@@ -114,29 +114,29 @@ echo
|
||||
check_eq \
|
||||
" 1. name absent — the default" \
|
||||
"[0]" \
|
||||
-k none -s "$DEPTH $DK @d@" -d
|
||||
--klammersets none -s "$DEPTH $DK @d@" -d
|
||||
|
||||
check_eq \
|
||||
" 2. name written alone — the type's alone value" \
|
||||
"[3]" \
|
||||
-k none -s "$DEPTH $DK @d :n @" -d
|
||||
--klammersets none -s "$DEPTH $DK @d :n @" -d
|
||||
|
||||
check_eq \
|
||||
" 3. name written with a value — that value" \
|
||||
"[7]" \
|
||||
-k none -s "$DEPTH $DK @d :n 7 @" -d
|
||||
--klammersets none -s "$DEPTH $DK @d :n 7 @" -d
|
||||
|
||||
# --- A type with an alone value but no default ---
|
||||
|
||||
check_eq \
|
||||
" 4. no default declared — absent is empty" \
|
||||
"[]" \
|
||||
-k none -s "$MARK $MK @m@" -d
|
||||
--klammersets none -s "$MARK $MK @m@" -d
|
||||
|
||||
check_eq \
|
||||
" 5. no default declared — alone still applies" \
|
||||
"[*]" \
|
||||
-k none -s "$MARK $MK @m :c @" -d
|
||||
--klammersets none -s "$MARK $MK @m :c @" -d
|
||||
|
||||
# --- The alone value belongs to the type, so every parameter of that
|
||||
# type gets it, and a parameter default does not disturb it ---
|
||||
@@ -144,73 +144,73 @@ check_eq \
|
||||
check_eq \
|
||||
" 6. two parameters of one type share the alone value" \
|
||||
"[3][3]" \
|
||||
-k none -s "$DEPTH @@d2 :a.depth :b.depth : [*a*][*b*] @@ @d2 :a :b @" -d
|
||||
--klammersets none -s "$DEPTH @@d2 :a.depth :b.depth : [*a*][*b*] @@ @d2 :a :b @" -d
|
||||
|
||||
check_eq \
|
||||
" 7. a parameter default overrides the type default, not the alone value" \
|
||||
"[5]|[3]" \
|
||||
-k none -s "$DEPTH @@d3 :n.depth 5 : [*n*] @@ @d3@|@d3 :n @" -d
|
||||
--klammersets none -s "$DEPTH @@d3 :n.depth 5 : [*n*] @@ @d3@|@d3 :n @" -d
|
||||
|
||||
# --- bool: the convention that a bare boolean option means true ---
|
||||
|
||||
check_eq \
|
||||
" 8. bool written alone is true" \
|
||||
"[true]" \
|
||||
-k none -s '@@b :f.bool : [*f*] @@ @b :f @' -d
|
||||
--klammersets none -s '@@b :f.bool : [*f*] @@ @b :f @' -d
|
||||
|
||||
check_eq \
|
||||
" 9. bool written with false stays false" \
|
||||
"[false]" \
|
||||
-k none -s '@@b :f.bool : [*f*] @@ @b :f false @' -d
|
||||
--klammersets none -s '@@b :f.bool : [*f*] @@ @b :f false @' -d
|
||||
|
||||
check_eq \
|
||||
"10. bool absent with no default is empty" \
|
||||
"[]" \
|
||||
-k none -s '@@b :f.bool : [*f*] @@ @b@' -d
|
||||
--klammersets none -s '@@b :f.bool : [*f*] @@ @b@' -d
|
||||
|
||||
check_eq \
|
||||
"11. bool absent with a true parameter default" \
|
||||
"[true]" \
|
||||
-k none -s '@@b :f.bool true : [*f*] @@ @b@' -d
|
||||
--klammersets none -s '@@b :f.bool true : [*f*] @@ @b@' -d
|
||||
|
||||
check_eq \
|
||||
"12. bool false explicitly against a true default" \
|
||||
"[false]" \
|
||||
-k none -s '@@b :f.bool true : [*f*] @@ @b :f false @' -d
|
||||
--klammersets none -s '@@b :f.bool true : [*f*] @@ @b :f false @' -d
|
||||
|
||||
# --- The value reaching @eval ---
|
||||
|
||||
check_eq \
|
||||
"13. python value of a bool written alone" \
|
||||
"True" \
|
||||
-k none -s '@@b :f.bool : @eval repr(K.f) @ @@ @b :f @' -d
|
||||
--klammersets none -s '@@b :f.bool : @eval repr(K.f) @ @@ @b :f @' -d
|
||||
|
||||
check_eq \
|
||||
"14. python value of an absent bool" \
|
||||
"None" \
|
||||
-k none -s '@@b :f.bool : @eval repr(K.f) @ @@ @b@' -d
|
||||
--klammersets none -s '@@b :f.bool : @eval repr(K.f) @ @@ @b@' -d
|
||||
|
||||
check_eq \
|
||||
"15. python value of a user type written alone" \
|
||||
"'3'" \
|
||||
-k none -s "$DEPTH @@d4 :n.depth : @eval repr(K.n) @ @@ @d4 :n @" -d
|
||||
--klammersets none -s "$DEPTH @@d4 :n.depth : @eval repr(K.n) @ @@ @d4 :n @" -d
|
||||
|
||||
# --- Types that cannot delimit a bare option name ---
|
||||
|
||||
check_error \
|
||||
"16. :alone refused on a type matching running text" \
|
||||
"cannot declare an :alone value" \
|
||||
-k none -s '@@@argtype loose | anything at all :alone x @@@' -d
|
||||
--klammersets none -s '@@@argtype loose | anything at all :alone x @@@' -d
|
||||
|
||||
check_error \
|
||||
"17. :alone refused on an explicit match-everything pattern" \
|
||||
"cannot declare an :alone value" \
|
||||
-k none -s '@@@argtype loose | anything :pattern (?:.^|\n)* :alone x @@@' -d
|
||||
--klammersets none -s '@@@argtype loose | anything :pattern (?:.^|\n)* :alone x @@@' -d
|
||||
|
||||
check_error \
|
||||
"18. an alone value must match its own type's pattern" \
|
||||
"does not match its own pattern" \
|
||||
-k none -s '@@@argtype depth | a depth :pattern \d+ :alone many @@@' -d
|
||||
--klammersets none -s '@@@argtype depth | a depth :pattern \d+ :alone many @@@' -d
|
||||
|
||||
# --- Delimitation: an option value still runs to the next bar or option
|
||||
# name, so text after a bare name is taken as the value and rejected
|
||||
@@ -219,19 +219,19 @@ check_error \
|
||||
check_error \
|
||||
"19. text after a bare option name is taken as its value" \
|
||||
"does not match" \
|
||||
-k none -s "$DEPTH $DK @d :n some words @" -d
|
||||
--klammersets none -s "$DEPTH $DK @d :n some words @" -d
|
||||
|
||||
check_eq \
|
||||
"20. a bar separates a bare option name from following text" \
|
||||
"[3]two" \
|
||||
-k none -s "$DEPTH @@d5 :n.depth | t : [*n*]*t* @@ @d5 :n | two @" -d
|
||||
--klammersets none -s "$DEPTH @@d5 :n.depth | t : [*n*]*t* @@ @d5 :n | two @" -d
|
||||
|
||||
# --- A type with no alone value is unchanged: a bare name is empty ---
|
||||
|
||||
check_eq \
|
||||
"21. bare option of a type with no alone value is empty" \
|
||||
"[]" \
|
||||
-k none -s '@@s :t.word : [*t*] @@ @s :t @' -d
|
||||
--klammersets none -s '@@s :t.word : [*t*] @@ @s :t @' -d
|
||||
|
||||
echo
|
||||
echo "================================"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# check_test.sh — Static checking of klammer applications ("ktext --check").
|
||||
# check_test.sh — Static checking of klammer applications ("kdiag --check").
|
||||
#
|
||||
# The engine applies klammers as it meets them, so it can only complain about
|
||||
# what it reaches. Two things it therefore never reaches:
|
||||
@@ -24,14 +24,17 @@
|
||||
# literal parameters, which are code, filenames, and raw text -- not
|
||||
# applications.
|
||||
#
|
||||
# Engine tier: no klammer set (-k none), every klammer defined inline.
|
||||
# Engine tier: kdiag loads no klammerset, so every klammer is defined inline.
|
||||
# The one case that needs the SKS reads it explicitly with "@read" -- which is
|
||||
# how a klammerset reaches kdiag at all: the symbol mechanism is deliberately
|
||||
# absent there, so that kdiag still works when symbol resolution is what broke.
|
||||
#
|
||||
# Usage: ./check_test.sh
|
||||
# Exit code: 0 if all tests pass, 1 otherwise.
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
KTEXT=ktext
|
||||
KDIAG=kdiag
|
||||
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
|
||||
|
||||
red=$'\033[31m'
|
||||
@@ -39,7 +42,7 @@ green=$'\033[32m'
|
||||
bold=$'\033[1m'
|
||||
reset=$'\033[0m'
|
||||
|
||||
# check_finds TEST_NAME PATTERN KTEXT_ARGS...
|
||||
# check_finds TEST_NAME PATTERN KDIAG_ARGS...
|
||||
# --check must exit nonzero and report PATTERN.
|
||||
check_finds() {
|
||||
local test_name="$1"
|
||||
@@ -47,7 +50,7 @@ check_finds() {
|
||||
shift 2
|
||||
|
||||
local output status
|
||||
output=$("$KTEXT" --check "$@" 2>&1)
|
||||
output=$("$KDIAG" --process --check "$@" 2>&1)
|
||||
status=$?
|
||||
|
||||
if [ $status -eq 0 ]; then
|
||||
@@ -65,14 +68,14 @@ check_finds() {
|
||||
fi
|
||||
}
|
||||
|
||||
# check_clean TEST_NAME KTEXT_ARGS...
|
||||
# check_clean TEST_NAME KDIAG_ARGS...
|
||||
# --check must exit 0 and report no diagnostics.
|
||||
check_clean() {
|
||||
local test_name="$1"
|
||||
shift
|
||||
|
||||
local output status
|
||||
output=$("$KTEXT" --check "$@" 2>&1)
|
||||
output=$("$KDIAG" --process --check "$@" 2>&1)
|
||||
status=$?
|
||||
|
||||
if [ $status -eq 0 ] && echo "$output" | grep -q "0 diagnostics"; then
|
||||
@@ -85,7 +88,7 @@ check_clean() {
|
||||
fi
|
||||
}
|
||||
|
||||
# check_count TEST_NAME N KTEXT_ARGS...
|
||||
# check_count TEST_NAME N KDIAG_ARGS...
|
||||
# --check must report exactly N diagnostics.
|
||||
check_count() {
|
||||
local test_name="$1"
|
||||
@@ -93,7 +96,7 @@ check_count() {
|
||||
shift 2
|
||||
|
||||
local output got
|
||||
output=$("$KTEXT" --check "$@" 2>&1)
|
||||
output=$("$KDIAG" --process --check "$@" 2>&1)
|
||||
got=$(echo "$output" | sed -nE 's/^([0-9]+) diagnostics?,.*/\1/p')
|
||||
|
||||
if [ "$got" = "$expected" ]; then
|
||||
@@ -116,87 +119,95 @@ echo
|
||||
|
||||
check_finds " 1. undefined klammer in an unselected @cond branch (in a body)" \
|
||||
"@nosuch is not defined" \
|
||||
-k none -s "@@pick p : @cond *p* | @nosuch x @ | ok @ @@ @pick false @"
|
||||
"@@pick p : @cond *p* | @nosuch x @ | ok @ @@ @pick false @"
|
||||
|
||||
check_finds " 2. wrong arity in an unselected @cond branch (in a body)" \
|
||||
"is given 3" \
|
||||
-k none -s "$GREET @@pick p : @cond *p* | @greet a | b | c @ | ok @ @@ @pick false @"
|
||||
"$GREET @@pick p : @cond *p* | @greet a | b | c @ | ok @ @@ @pick false @"
|
||||
|
||||
# A @cond written at the top level of a DOCUMENT is resolved when the file is
|
||||
# read, so its unselected branch is gone before anything can be checked. This
|
||||
# test records that limitation rather than asserting the behavior is right; see
|
||||
# notes/Klammertext_improvements.md, "When @cond is resolved".
|
||||
check_clean " 2a. LIMITATION: a top-level @cond branch is resolved before checking" \
|
||||
-k none -s "@cond false | @nosuch x @ | ok @"
|
||||
# A top-level @cond branch is now checked like any other. It was the one
|
||||
# documented gap in the checker: a @cond written at the top level of a DOCUMENT
|
||||
# was resolved when the file was READ, so its unselected branch was gone before
|
||||
# anything could look at it. Resolving @cond at APPLICATION time instead
|
||||
# (notes/Klammertext_improvements.md §4.2, decided 2026-08-15) closed it -- the
|
||||
# branches survive read, so both are visible to a pass that applies nothing.
|
||||
check_finds " 2a. a top-level @cond branch is checked too" \
|
||||
"@nosuch is not defined" \
|
||||
"@cond false | @nosuch x @ | ok @"
|
||||
check_finds " 2b. ... including the branch that WOULD be selected" \
|
||||
"@nosuch is not defined" \
|
||||
"@cond true | @nosuch x @ | ok @"
|
||||
|
||||
check_finds " 3. undefined klammer in a body that is never applied" \
|
||||
"@nosuch is not defined" \
|
||||
-k none -s "@@unused : @nosuch x @ @@ nothing applies it"
|
||||
"@@unused : @nosuch x @ @@ nothing applies it"
|
||||
|
||||
check_finds " 4. the body it was found in is named" \
|
||||
"in body of @unused" \
|
||||
-k none -s "@@unused : @nosuch x @ @@ nothing applies it"
|
||||
"@@unused : @nosuch x @ @@ nothing applies it"
|
||||
|
||||
# --- Arity ---
|
||||
|
||||
check_finds " 5. too few positional arguments" \
|
||||
"needs 2 positional arguments but is given 1" \
|
||||
-k none -s '@@pair a | b : *a**b* @@ @pair x @'
|
||||
'@@pair a | b : *a**b* @@ @pair x @'
|
||||
|
||||
check_finds " 6. too many positional arguments" \
|
||||
"takes 1 positional argument but is given 2" \
|
||||
-k none -s "$GREET @greet a | b @"
|
||||
"$GREET @greet a | b @"
|
||||
|
||||
check_finds " 7. undefined optional argument" \
|
||||
'has no optional argument ":nope"' \
|
||||
-k none -s "$GREET @greet a :nope 1 @"
|
||||
"$GREET @greet a :nope 1 @"
|
||||
|
||||
check_finds " 8. the accepted optional arguments are listed" \
|
||||
"It accepts: :loud" \
|
||||
-k none -s '@@greet name :loud : *name* @@ @greet a :nope 1 @'
|
||||
'@@greet name :loud : *name* @@ @greet a :nope 1 @'
|
||||
|
||||
check_clean " 9. a rest argument accepts extra positional arguments" \
|
||||
-k none -s '@@many a | rest.rest : *a* @@ @many x | y | z @'
|
||||
'@@many a | rest.rest : *a* @@ @many x | y | z @'
|
||||
|
||||
# --- Nesting. A bar or an option name belonging to a nested klammer is not
|
||||
# this klammer's; the checker counts at depth 0, as @cond does. ---
|
||||
|
||||
check_clean "10. nested klammer's bars are not counted as the outer's" \
|
||||
-k none -s '@@frac a | b : *a*/*b* @@ @@one x : [*x*] @@ @one @frac 1 | 2 @ @'
|
||||
'@@frac a | b : *a*/*b* @@ @@one x : [*x*] @@ @one @frac 1 | 2 @ @'
|
||||
|
||||
check_clean "11. nested klammer's option name is not counted as the outer's" \
|
||||
-k none -s '@@inner a :flag : *a* @@ @@outer x : [*x*] @@ @outer @inner q :flag y @ @'
|
||||
'@@inner a :flag : *a* @@ @@outer x : [*x*] @@ @outer @inner q :flag y @ @'
|
||||
|
||||
# --- Target coverage ---
|
||||
|
||||
check_finds "12. klammer not defined for a target" \
|
||||
'is not defined for the target "tex"' \
|
||||
-k none -s '@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@only.html : H @@ @only@'
|
||||
'@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@only.html : H @@ @only@'
|
||||
|
||||
check_clean "13. defined for every target is clean" \
|
||||
-k none -s '@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@both : B @@ @both@'
|
||||
'@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@both : B @@ @both@'
|
||||
|
||||
# --- What the checker deliberately does not see ---
|
||||
|
||||
# In a body, so the @eval is not evaluated at read time: what is being tested
|
||||
# is that the checker does not read the eval's ARGUMENT as an application.
|
||||
check_clean "14. @eval argument content is code, not applications" \
|
||||
-k none -s '@@w : @eval len("@nosuch") @ @@'
|
||||
'@@w : @eval len("@nosuch") @ @@'
|
||||
|
||||
check_clean "15. a literal parameter's content is raw text" \
|
||||
-k none -s '@@lit t.literal : *t* @@ @lit @nosuch x @ lit@'
|
||||
'@@lit t.literal : *t* @@ @lit @nosuch x @ lit@'
|
||||
|
||||
# --- Reporting ---
|
||||
|
||||
check_count "16. a target-independent fault is reported once, not per target" \
|
||||
1 \
|
||||
-k none -s '@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@g : @nosuch@ @@'
|
||||
'@@@target html | HTML output @@@ @@@target tex | TeX output @@@ @@g : @nosuch@ @@'
|
||||
|
||||
check_clean "17. a correct document checks clean" \
|
||||
-k none -s "$GREET @greet World @"
|
||||
"$GREET @greet World @"
|
||||
|
||||
# An absolute path: "make -C tst test" runs from tst/, and @read resolves
|
||||
# against the current directory.
|
||||
check_clean "18. the Standard Klammer Set checks clean" \
|
||||
-s 'x'
|
||||
"@read $K/sks/sks.k @"
|
||||
|
||||
echo
|
||||
echo "============================="
|
||||
|
||||
@@ -113,6 +113,76 @@ else
|
||||
echo "SKIP 13/14. two positionals (argv_test not built; run make -C tst)"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "-- the output policy: three categories, two streams --"
|
||||
# The three commands display text in exactly three cases:
|
||||
# 1. logging under "-v" > 0 -> STDERR
|
||||
# 2. an error before termination -> STDERR
|
||||
# 3. output the user asked for -> STDOUT
|
||||
# For ktext, category 3 is a DOCUMENT that may be piped, so nothing else may
|
||||
# share the stream. It did: K::log and msg() both wrote to stdout, so
|
||||
# "ktext -d -v 1 > doc.txt" put the whole argument dump inside the document.
|
||||
OUTF=$(mktemp /tmp/kout.XXXXXX); ERRF=$(mktemp /tmp/kerr.XXXXXX)
|
||||
trap 'rm -f "$OUTF" "$ERRF"' EXIT
|
||||
|
||||
ktext --klammersets none -s 'hello' -d -v 1 > "$OUTF" 2> "$ERRF"
|
||||
if [ "$(cat "$OUTF")" = "hello" ]; then
|
||||
echo "${green}PASS${reset} 15. stdout under -d -v 1 is the document, nothing else"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} 15. stdout carried more than the document"
|
||||
echo " got: $(head -3 "$OUTF")"; FAIL=$((FAIL+1))
|
||||
fi
|
||||
if [ -s "$ERRF" ]; then
|
||||
echo "${green}PASS${reset} 16. ... and the -v 1 logging went to stderr"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} 16. -v 1 produced no stderr"; FAIL=$((FAIL+1))
|
||||
fi
|
||||
# -v 0 adds nothing anywhere: silence is the default, on both streams.
|
||||
ktext --klammersets none -s 'hello' -d > "$OUTF" 2> "$ERRF"
|
||||
if [ "$(cat "$OUTF")" = "hello" ] && [ ! -s "$ERRF" ]; then
|
||||
echo "${green}PASS${reset} 17. -v 0 is silent on both streams"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} 17. -v 0 was not silent (stderr: $(head -2 "$ERRF"))"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
# An error is category 2: stderr, and nothing on stdout to confuse a pipe.
|
||||
ktext --klammersets none -s '@nosuch x @' -d > "$OUTF" 2> "$ERRF"
|
||||
if [ ! -s "$OUTF" ] && [ -s "$ERRF" ]; then
|
||||
echo "${green}PASS${reset} 18. an error goes to stderr, leaving stdout empty"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} 18. error stream discipline (stdout: $(head -2 "$OUTF"))"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
# Colour is emitted only to a terminal, so a redirected stream never carries
|
||||
# escape sequences -- into a document, a pipe, or a captured log.
|
||||
for pair in "ktext:--klammersets none -s hello -d" "kdesc:-k table" "kdiag:@i-x"; do
|
||||
cmd=${pair%%:*}; rest=${pair#*:}
|
||||
# shellcheck disable=SC2086
|
||||
$cmd $rest > "$OUTF" 2> "$ERRF"
|
||||
if ! grep -q $'\033' "$OUTF" && ! grep -q $'\033' "$ERRF"; then
|
||||
echo "${green}PASS${reset} 19. no escape sequences from $cmd when redirected"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} 19. $cmd emitted colour to a non-terminal"; FAIL=$((FAIL+1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "-- a bare command is a request, not a failure --"
|
||||
# Usage is the result being asked for: stdout, exit 0. It exited 1, so
|
||||
# "kdesc && echo ok" reported failure for a successful help request.
|
||||
for cmd in ktext kdesc kdiag; do
|
||||
$cmd > "$OUTF" 2> "$ERRF"; status=$?
|
||||
if [ $status -eq 0 ] && [ -s "$OUTF" ] && ! [ -s "$ERRF" ]; then
|
||||
echo "${green}PASS${reset} 20. $cmd with no arguments: usage on stdout, exit 0"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} 20. $cmd bare: exit $status, stdout $(wc -l < "$OUTF") lines, stderr $(wc -l < "$ERRF") lines"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo "===================="
|
||||
echo "Results: ${PASS} passed, ${FAIL} failed"
|
||||
|
||||
@@ -117,6 +117,11 @@ check_error() {
|
||||
|
||||
FRAC='@@frac a | b : *a*/*b* @@'
|
||||
|
||||
# A file for the @read non-strictness cases (21-21b).
|
||||
READ_FIXTURE=$(mktemp /tmp/cond_read.XXXXXX)
|
||||
printf 'READ-FIXTURE\n' > "$READ_FIXTURE"
|
||||
trap 'rm -f "$READ_FIXTURE"' EXIT
|
||||
|
||||
echo "${bold}@cond argument delimitation tests${reset}"
|
||||
echo "================================="
|
||||
echo
|
||||
@@ -174,6 +179,99 @@ check_error "14. zero bars is still an error" \
|
||||
"one or two bar characters" \
|
||||
-s '@cond true @' -d
|
||||
|
||||
# --- The predicate relation: TOTAL AND STRICT (Andy, 2026-08-15) ---
|
||||
#
|
||||
# Deciding notes/Klammertext_improvements.md §4.1. There is a defined true
|
||||
# set, a defined false set, and anything else is an error AT THE @cond. It was
|
||||
# partial until then: is_true() recognized three strings and everything else
|
||||
# took the false branch, so a misspelled variable, a "TRUE", a "yes", or a
|
||||
# Python traceback all silently selected a branch. A warning had made that
|
||||
# visible while the policy was open; it never fired on the SKS, which is the
|
||||
# evidence that the blast radius is small.
|
||||
|
||||
echo
|
||||
echo "-- the predicate relation --"
|
||||
|
||||
for p in true True 1; do
|
||||
check_eq "15. \"$p\" is true" "T" -s "@cond $p | T | F @" -d
|
||||
done
|
||||
for p in false False 0; do
|
||||
check_eq "16. \"$p\" is false" "F" -s "@cond $p | T | F @" -d
|
||||
done
|
||||
|
||||
# Empty stays FALSE, and load-bearing: an optional argument that was not
|
||||
# written substitutes as empty, which is what carries the "@cond *opt*" idiom.
|
||||
# The entangled sub-question in §4.1 -- empty means false, or means "not
|
||||
# supplied"? -- is answered "false" by that use.
|
||||
check_eq "17. an absent optional argument is false" "F" \
|
||||
-s '@@g :opt : @cond *opt* | T | F @ @@ @g@' -d
|
||||
check_eq "17a. ... and the same argument written true is true" "T" \
|
||||
-s '@@g :opt : @cond *opt* | T | F @ @@ @g :opt true @' -d
|
||||
|
||||
check_error "18. an unrecognized predicate is an error, not false" \
|
||||
"is not a truth value" \
|
||||
-s '@cond yes | T | F @' -d
|
||||
check_error "18a. ... including a near miss of a true value" \
|
||||
"is not a truth value" \
|
||||
-s '@cond TRUE | T | F @' -d
|
||||
# A state variable in a klammer BODY works: a body is processed at application
|
||||
# time, after substitution, so @cond sees the value.
|
||||
check_eq "19. a state variable in a body reaches the @cond" "T" \
|
||||
-s '@@@state Flag :value true @@@ @@g : @cond *Flag* | T | F @ @@ @g@' -d
|
||||
|
||||
# A top-level state variable reaches the predicate too, since @cond is resolved
|
||||
# at APPLICATION time (notes/Klammertext_improvements.md §4.2, decided
|
||||
# 2026-08-15). It did not until then: a top-level @cond was resolved when the
|
||||
# file was READ, which is before state substitution, so it saw the literal
|
||||
# "*Flag*" and silently took the false branch -- the WRONG answer for a flag
|
||||
# whose value was true. The document now behaves like a klammer body: its
|
||||
# state variables are bound before its conditionals are decided.
|
||||
check_eq "19a. a top-level state variable reaches the @cond" "T" \
|
||||
-s '@@@state Flag :value true @@@ @cond *Flag* | T | F @' -d
|
||||
check_eq "19b. ... and selects the false branch when it is false" "F" \
|
||||
-s '@@@state Flag :value false @@@ @cond *Flag* | T | F @' -d
|
||||
|
||||
# --- Non-strictness: nothing in a discarded branch runs ---
|
||||
#
|
||||
# doc/cond_evaluation_order.md states this ("with side-effecting @read/@eval,
|
||||
# wrong ... must not read the missing file"). @read honoured it; @eval did not,
|
||||
# because the eval pass swept the list before the cond pass did. Both honour it
|
||||
# now: mark_cond_content() makes a branch inert BEFORE either pass runs.
|
||||
|
||||
check_eq "21. a @read in a discarded branch is not performed" "ok" \
|
||||
-s "@cond false | @read $READ_FIXTURE @ | ok @" -d
|
||||
check_eq "21a. ... and IS performed when the branch is selected" "READ-FIXTURE" \
|
||||
-s "@cond true | @read $READ_FIXTURE @ | ok @" -d
|
||||
# The case the design document names: the file need not even exist.
|
||||
check_eq "21b. ... so a missing file in a discarded branch is not an error" "ok" \
|
||||
-s '@cond false | @read /nonexistent/no-such-file.txt @ | ok @' -d
|
||||
|
||||
# An @eval side effect is the observable test: the branch either touched the
|
||||
# file or it did not.
|
||||
SIDE=$(mktemp -u /tmp/cond_side.XXXXXX)
|
||||
"$KTEXT" --klammersets none -s "@cond false | @eval :shell touch $SIDE @ | ok @" -d >/dev/null 2>&1
|
||||
if [ -f "$SIDE" ]; then
|
||||
echo "${red}FAIL${reset} 22. an @eval in a discarded branch ran"; FAIL=$((FAIL+1)); rm -f "$SIDE"
|
||||
else
|
||||
echo "${green}PASS${reset} 22. an @eval in a discarded branch does not run"; PASS=$((PASS+1))
|
||||
fi
|
||||
"$KTEXT" --klammersets none -s "@cond true | @eval :shell touch $SIDE @ | ok @" -d >/dev/null 2>&1
|
||||
if [ -f "$SIDE" ]; then
|
||||
echo "${green}PASS${reset} 22a. ... and does run when the branch is selected"; PASS=$((PASS+1)); rm -f "$SIDE"
|
||||
else
|
||||
echo "${red}FAIL${reset} 22a. an @eval in the selected branch did not run"; FAIL=$((FAIL+1))
|
||||
fi
|
||||
|
||||
# The PREDICATE is always evaluated -- a conditional that could not compute its
|
||||
# own predicate would be useless. Only the branches are non-strict.
|
||||
check_eq "23. the predicate is evaluated even though the branches are not" "yes" \
|
||||
-s '@cond @eval 1==1 @ | yes | no @' -d
|
||||
# The recognized sets are named in the message, since the whole point is that
|
||||
# the writer has to know what they are.
|
||||
check_error "20. the message names the recognized values" \
|
||||
"true, True, 1" \
|
||||
-s '@cond yes | T | F @' -d
|
||||
|
||||
rm -f /tmp/cond_test_err.$$
|
||||
|
||||
echo
|
||||
|
||||
@@ -17,7 +17,10 @@
|
||||
# REPORTED, and a companion case checks that rendering is unaffected.
|
||||
#
|
||||
# Engine tier: the fixtures in tst/coverage/ declare their own targets with
|
||||
# @@@target, so no klammer set is involved.
|
||||
# @@@target, so no klammer set is involved -- hence "--klammersets none".
|
||||
# Since the 2026-08-14 redesign "-i" ADDS a file to whatever klammersets are
|
||||
# loaded (the SKS by default) rather than replacing them, so the exclusion has
|
||||
# to be explicit or every count here would include the SKS.
|
||||
#
|
||||
# Usage: ./coverage_test.sh (needs KLAMMERTEXT_HOME set; kdesc on PATH)
|
||||
# Exit code: 0 if all tests pass, 1 otherwise.
|
||||
@@ -34,23 +37,50 @@ green=$'\033[32m'
|
||||
bold=$'\033[1m'
|
||||
reset=$'\033[0m'
|
||||
|
||||
# Each fixture is analysed once; the tests match against the saved report.
|
||||
declare -A REPORT VREPORT
|
||||
|
||||
# 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
|
||||
|
||||
# Each fixture is analysed once and its report SAVED TO A FILE, keyed by name.
|
||||
#
|
||||
# It used to be a pair of associative arrays. "declare -A" is bash 4, and
|
||||
# macOS ships bash 3.2 as /bin/bash -- where the declaration fails and every
|
||||
# string subscript then evaluates to 0, so all five fixtures overwrote one
|
||||
# slot and the whole suite compared the wrong report against the wrong test.
|
||||
# Files have no such floor and read the same on both systems.
|
||||
REPORTS=$(mktemp -d /tmp/coverage_reports.XXXXXX)
|
||||
trap 'rm -rf "$REPORTS"' EXIT
|
||||
|
||||
for f in basic undecidable cycle split clean; do
|
||||
REPORT[$f]=$(timeout 30 "$KDESC" -i "$DIR/$f.k" --coverage 2>&1 |
|
||||
sed 's/\x1b\[[0-9;]*m//g')
|
||||
limited "$KDESC" --klammersets none -i "$DIR/$f.k" --coverage 2>&1 |
|
||||
sed 's/\x1b\[[0-9;]*m//g' > "$REPORTS/$f"
|
||||
# "all" adds the source-file column AND the empty problem categories.
|
||||
VREPORT[$f]=$(timeout 30 "$KDESC" -i "$DIR/$f.k" --coverage all 2>&1 |
|
||||
sed 's/\x1b\[[0-9;]*m//g')
|
||||
if [ -z "${REPORT[$f]}" ] || [ -z "${VREPORT[$f]}" ]; then
|
||||
limited "$KDESC" --klammersets none -i "$DIR/$f.k" --coverage all 2>&1 |
|
||||
sed 's/\x1b\[[0-9;]*m//g' > "$REPORTS/$f.all"
|
||||
if [ ! -s "$REPORTS/$f" ] || [ ! -s "$REPORTS/$f.all" ]; then
|
||||
echo "${red}FAIL${reset} $f.k produced no report"; FAIL=$((FAIL+1))
|
||||
fi
|
||||
done
|
||||
|
||||
# report FIXTURE [all] — the saved report, on stdout.
|
||||
report() { cat "$REPORTS/$1"; }
|
||||
vreport() { cat "$REPORTS/$1.all"; }
|
||||
|
||||
# vlacks NAME FIXTURE REGEX — the VERBOSE report does NOT match REGEX.
|
||||
vlacks() {
|
||||
local name="$1" fixture="$2" rgx="$3"
|
||||
if printf '%s\n' "${VREPORT[$fixture]}" | grep -Eq "$rgx"; then
|
||||
if vreport "$fixture" | grep -Eq "$rgx"; then
|
||||
echo "${red}FAIL${reset} $name — unexpected match: $rgx"; FAIL=$((FAIL+1))
|
||||
else
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
@@ -60,7 +90,7 @@ vlacks() {
|
||||
# vhas NAME FIXTURE REGEX — the VERBOSE report matches REGEX.
|
||||
vhas() {
|
||||
local name="$1" fixture="$2" rgx="$3"
|
||||
if printf '%s\n' "${VREPORT[$fixture]}" | grep -Eq "$rgx"; then
|
||||
if vreport "$fixture" | grep -Eq "$rgx"; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name"
|
||||
@@ -71,7 +101,7 @@ vhas() {
|
||||
# has NAME FIXTURE REGEX — the report matches REGEX.
|
||||
has() {
|
||||
local name="$1" fixture="$2" rgx="$3"
|
||||
if printf '%s\n' "${REPORT[$fixture]}" | grep -Eq "$rgx"; then
|
||||
if report "$fixture" | grep -Eq "$rgx"; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $name"
|
||||
@@ -86,7 +116,7 @@ has() {
|
||||
section_lacks() {
|
||||
local name="$1" fixture="$2" header="$3" rgx="$4"
|
||||
local body
|
||||
body=$(printf '%s\n' "${REPORT[$fixture]}" |
|
||||
body=$(report "$fixture" |
|
||||
awk -v h="$header" 'index($0, h) == 1 { f = 1; next } f && /^$/ { exit } f')
|
||||
if [ -z "$body" ]; then
|
||||
echo "${red}FAIL${reset} $name — section [$header] not found"; FAIL=$((FAIL+1)); return
|
||||
@@ -101,7 +131,7 @@ section_lacks() {
|
||||
# lacks NAME FIXTURE REGEX — the report does NOT match REGEX.
|
||||
lacks() {
|
||||
local name="$1" fixture="$2" rgx="$3"
|
||||
if printf '%s\n' "${REPORT[$fixture]}" | grep -Eq "$rgx"; then
|
||||
if report "$fixture" | grep -Eq "$rgx"; then
|
||||
echo "${red}FAIL${reset} $name — unexpected match: $rgx"; FAIL=$((FAIL+1))
|
||||
else
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
@@ -243,7 +273,7 @@ echo
|
||||
echo "-- the analysis changes nothing --"
|
||||
# Coverage is a report, not a policy: a klammer whose general body cannot be
|
||||
# interpreted is still offered to every target, exactly as before.
|
||||
out=$("$KTEXT" -k none -s '@@@target ta | Target A @@@
|
||||
out=$("$KTEXT" --klammersets none -s '@@@target ta | Target A @@@
|
||||
@@e : @eval 6 * 7 @ @@ x @e@' -t ta -d 2>&1 | tr -d '\n ')
|
||||
if [ "$out" = "x42" ]; then
|
||||
echo "${green}PASS${reset} 22. an underivable klammer still renders"; PASS=$((PASS+1))
|
||||
|
||||
@@ -62,7 +62,16 @@ check_eq() {
|
||||
fi
|
||||
}
|
||||
|
||||
# check_warn NAME EXPECTED KTEXT_ARGS... — exit 0, stdout==EXPECTED, AND a warning on stderr.
|
||||
# check_warn NAME EXPECTED KTEXT_ARGS... — exit 0, stdout==EXPECTED, the
|
||||
# override REPORTED at "-v 1" and SILENT at the default verbosity.
|
||||
#
|
||||
# It was a warning until 2026-08-15. ":::" exists to override, and a klammer
|
||||
# set the user did not write may be overridden by design (TODO #33), so the
|
||||
# notice fired on the sanctioned use of a feature: a warning nobody can act on
|
||||
# is not a warning. It is still worth reporting, because the definition being
|
||||
# replaced usually lives in another file in another klammerset, which the user
|
||||
# cannot see from what they wrote -- so it is "-v 1" (see the output policy in
|
||||
# CLAUDE.md).
|
||||
check_warn() {
|
||||
local name="$1" expected="$2"; shift 2
|
||||
local out status err
|
||||
@@ -72,8 +81,14 @@ check_warn() {
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "${red}FAIL${reset} $name — ktext exited $status"; FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
if ! printf '%s' "$err" | grep -qiF "warning"; then
|
||||
echo "${red}FAIL${reset} $name — expected a warning, got none"; FAIL=$((FAIL+1)); return
|
||||
if [ -s "$ERR" ]; then
|
||||
echo "${red}FAIL${reset} $name — the default run was not silent: $(head -1 "$ERR")"
|
||||
FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
"$KTEXT" "$@" -v 1 >/dev/null 2>"$ERR"
|
||||
err=$(cat "$ERR")
|
||||
if ! printf '%s' "$err" | grep -qiF "overridden"; then
|
||||
echo "${red}FAIL${reset} $name — no override reported at -v 1"; FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
if [ "$out" = "$expected" ]; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
# mac/machine.cpp), not the SKS. A target is a MACHINE construct — declared
|
||||
# with the @@@target system command, not owned by any klammer set — so the
|
||||
# idiomatic engine-level test defines its own fixture target inline and loads
|
||||
# no klammer set (`-k none`); it does not "avoid" the SKS so much as have no
|
||||
# no klammer set (`--klammersets none`); it does not "avoid" the SKS so much as have no
|
||||
# need of it. Target `t` here escapes `& -> AMP`, `_ -> UND`, `\ -> BSL`
|
||||
# (arbitrary tokens, easy to assert). The SKS's own targets (tex, html) and
|
||||
# the specific characters they declare are exercised by the SKS suite.
|
||||
@@ -75,29 +75,29 @@ echo "======================================================================="
|
||||
echo
|
||||
|
||||
# A self-contained fixture target, defined inline via the @@@target system
|
||||
# command, escaping & _ \ to distinct tokens. -k none loads no klammer set,
|
||||
# command, escaping & _ \ to distinct tokens. --klammersets none loads no klammer set,
|
||||
# so nothing below depends on the SKS.
|
||||
T='@@@target t | test target :escape & AMP _ UND \ BSL @@@'
|
||||
|
||||
check_eq " 1. top-level text: & escaped" 'A AMP B' -k none -t t -s "$T A & B"
|
||||
check_eq " 2. general klammer body: & escaped" 'A AMP B' -k none -t t -s "$T @@g : A & B @@ @g@"
|
||||
check_eq " 3. general klammer body: _ escaped" 'AUNDB' -k none -t t -s "$T @@g : A_B @@ @g@"
|
||||
check_eq " 4. general klammer body: backslash escaped" 'aBSLb' -k none -t t -s "$T @@g : a\\b @@ @g@"
|
||||
check_eq " 5. target-specific body: NOT escaped" 'A & B' -k none -t t -s "$T @@g.k : d @@ @@g.t :: A & B @@ @g@"
|
||||
check_eq " 6. general body ^'...'^ literal: NOT escaped" 'a&b' -k none -t t -s "$T @@g : ^'a&b'^ @@ @g@"
|
||||
check_eq " 7. nested target-native klammer survives escape" 'X AMP Y \newline Z' -k none -t t -s "$T @@n.t : \\newline @@ @@g : X & Y @n@ Z @@ @g@"
|
||||
check_eq " 1. top-level text: & escaped" 'A AMP B' --klammersets none -t t -s "$T A & B"
|
||||
check_eq " 2. general klammer body: & escaped" 'A AMP B' --klammersets none -t t -s "$T @@g : A & B @@ @g@"
|
||||
check_eq " 3. general klammer body: _ escaped" 'AUNDB' --klammersets none -t t -s "$T @@g : A_B @@ @g@"
|
||||
check_eq " 4. general klammer body: backslash escaped" 'aBSLb' --klammersets none -t t -s "$T @@g : a\\b @@ @g@"
|
||||
check_eq " 5. target-specific body: NOT escaped" 'A & B' --klammersets none -t t -s "$T @@g.k : d @@ @@g.t :: A & B @@ @g@"
|
||||
check_eq " 6. general body ^'...'^ literal: NOT escaped" 'a&b' --klammersets none -t t -s "$T @@g : ^'a&b'^ @@ @g@"
|
||||
check_eq " 7. nested target-native klammer survives escape" 'X AMP Y \newline Z' --klammersets none -t t -s "$T @@n.t : \\newline @@ @@g : X & Y @n@ Z @@ @g@"
|
||||
# 8-9: @eval inside a general body. Code (with underscores) must not be
|
||||
# escaped or Python breaks; the Klammertext it returns is klammer output and
|
||||
# must not be escaped either. chr(64) builds a literal '@' so the returned
|
||||
# klammer call is not parsed as one in this source line.
|
||||
check_eq " 8. general body @eval code NOT escaped" '3' -k none -t t -s "$T @@g : @eval (1).__add__(2) @ @@ @g@"
|
||||
check_eq " 9. general body @eval klammer result NOT escaped" '\textbf{hi}' -k none -t t -s "$T @@b.k s : d @@ @@b.t :: \\textbf{*s*} @@ @@g : @eval chr(64)+'b hi '+chr(64) @ @@ @g@"
|
||||
check_eq " 8. general body @eval code NOT escaped" '3' --klammersets none -t t -s "$T @@g : @eval (1).__add__(2) @ @@ @g@"
|
||||
check_eq " 9. general body @eval klammer result NOT escaped" '\textbf{hi}' --klammersets none -t t -s "$T @@b.k s : d @@ @@b.t :: \\textbf{*s*} @@ @@g : @eval chr(64)+'b hi '+chr(64) @ @@ @g@"
|
||||
# 10-11: an @eval result that still holds klammers is a GENERATOR (Klammertext
|
||||
# with data) -- its writer text is escaped for the target before the klammers
|
||||
# are applied; a result with no klammers is a RENDERER (final markup) -- left
|
||||
# untouched. The signal is "does the read-back result contain a klammer".
|
||||
check_eq "10. @eval generator: klammer result's data escaped" '[a AMP b]' -k none -t t -s "$T @@wrap z : [*z*] @@ @@g : @eval chr(64)+'wrap a & b '+chr(64) @ @@ @g@"
|
||||
check_eq "11. @eval renderer: final markup NOT escaped" 'raw & markup' -k none -t t -s "$T @@g : @eval 'raw & markup' @ @@ @g@"
|
||||
check_eq "10. @eval generator: klammer result's data escaped" '[a AMP b]' --klammersets none -t t -s "$T @@wrap z : [*z*] @@ @@g : @eval chr(64)+'wrap a & b '+chr(64) @ @@ @g@"
|
||||
check_eq "11. @eval renderer: final markup NOT escaped" 'raw & markup' --klammersets none -t t -s "$T @@g : @eval 'raw & markup' @ @@ @g@"
|
||||
|
||||
# 12-18: quoted KLAMMERTEXT specials (^@ ^| ^# ^^ ^: ^*) and ^'...'^ literal
|
||||
# regions. The katomizer strips the "^"; hide_special_katoms() and
|
||||
@@ -108,21 +108,21 @@ check_eq "11. @eval renderer: final markup NOT escaped" 'raw & markup'
|
||||
# leaked as a bare apply-end katom into re-read text ("A klammer ends
|
||||
# without a beginning"). Exact-match expectations also guard against KTESC
|
||||
# markers leaking into output.
|
||||
check_eq "12. quoted @ | # resolve to the characters" 'x @ | # y' -k none -t t -s "$T x ^@ ^| ^# y"
|
||||
check_eq "13. quoted ^ : * resolve to the characters" 'x ^ : * y' -k none -t t -s "$T x ^^ ^: ^* y"
|
||||
check_eq "14. ^'...'^ region: specials stay literal" 'a @ | b' -k none -t t -s "$T a ^' @ | '^ b"
|
||||
check_eq "15. general body: quoted @ resolves" 'x @ y' -k none -t t -s "$T @@g : x ^@ y @@ @g@"
|
||||
check_eq "12. quoted @ | # resolve to the characters" 'x @ | # y' --klammersets none -t t -s "$T x ^@ ^| ^# y"
|
||||
check_eq "13. quoted ^ : * resolve to the characters" 'x ^ : * y' --klammersets none -t t -s "$T x ^^ ^: ^* y"
|
||||
check_eq "14. ^'...'^ region: specials stay literal" 'a @ | b' --klammersets none -t t -s "$T a ^' @ | '^ b"
|
||||
check_eq "15. general body: quoted @ resolves" 'x @ y' --klammersets none -t t -s "$T @@g : x ^@ y @@ @g@"
|
||||
# 16: inside an @eval argument span a quoted special reaches the CODE as the
|
||||
# character (the span is skipped by hide_special_katoms, like the escape pass).
|
||||
check_eq "16. @eval code: quoted : reaches shell as ':'" 'x:y' -k none -t t -s "$T @@g : @eval :shell echo x^:y @ @@ @g@"
|
||||
check_eq "16. @eval code: quoted : reaches shell as ':'" 'x:y' --klammersets none -t t -s "$T @@g : @eval :shell echo x^:y @ @@ @g@"
|
||||
# 17: an @eval result emitting the two characters ^ @ is re-read as a quoted
|
||||
# special and survives to the output as a literal @ (the generator idiom for
|
||||
# a literal @; a bare @ in a result is a parse error by design).
|
||||
check_eq "17. @eval result ^@ survives read-back as @" '@' -k none -t t -s "$T @@g : @eval chr(94)+chr(64) @ @@ @g@"
|
||||
check_eq "17. @eval result ^@ survives read-back as @" '@' --klammersets none -t t -s "$T @@g : @eval chr(94)+chr(64) @ @@ @g@"
|
||||
# 18: a bare-Python :after_apply phase receives the RESOLVED result text
|
||||
# (K_result) and its return is taken as raw target text, not re-read as
|
||||
# Klammertext -- a resolved @ in the result must not be re-parsed.
|
||||
check_eq "18. :after_apply phase: raw result, @ intact" 'A @ B' -k none -t u -s '@@@target u | up :after_apply string.capwords @@@ a ^@ b'
|
||||
check_eq "18. :after_apply phase: raw result, @ intact" 'A @ B' --klammersets none -t u -s '@@@target u | up :after_apply string.capwords @@@ a ^@ b'
|
||||
|
||||
rm -f "$ERR"
|
||||
|
||||
|
||||
226
tst/eval_test.sh
Executable file
226
tst/eval_test.sh
Executable file
@@ -0,0 +1,226 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# eval_test.sh — the @eval primitive's contract with the outside world.
|
||||
#
|
||||
# @eval is the one primitive that reaches OUT of Klammertext, and until
|
||||
# 2026-08-15 nothing tested what it did with what came back. ":shell" is
|
||||
# covered here; the other modes (Python, :cpp, :haskell) are exercised
|
||||
# incidentally by other suites and can grow into this one.
|
||||
#
|
||||
# The two defects this suite exists to hold shut, both of them the same shape
|
||||
# as a msg() on the wrong stream -- output nobody chose to see, and a failure
|
||||
# nobody was told about:
|
||||
#
|
||||
# * The command's STDERR went straight to the user's terminal, unattributed
|
||||
# and unsuppressable. It is not the command's output in any of the three
|
||||
# policy categories (CLAUDE.md, "Command output policy"): it belongs to a
|
||||
# subprocess a klammer invoked, at a location the Locator can name. It is
|
||||
# now captured and reported at "-v 1".
|
||||
# * The EXIT STATUS was discarded, so a command that failed contributed its
|
||||
# partial output (or nothing) to the document and said nothing at all.
|
||||
#
|
||||
# NOT to be confused with "eval_test", the C++ diagnostic program built from
|
||||
# eval_test.cpp in this directory: that one constructs engine objects and prints
|
||||
# what it gets, for a person to read, and asserts nothing (see the `smoke`
|
||||
# target in tst/Makefile). This is the regression suite. The ".sh" is the only
|
||||
# thing distinguishing them, and it is the first such collision in tst/.
|
||||
#
|
||||
# Usage: ./eval_test.sh (needs KLAMMERTEXT_HOME set; ktext on PATH)
|
||||
# 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'
|
||||
|
||||
OUTF=$(mktemp /tmp/eval_out.XXXXXX)
|
||||
ERRF=$(mktemp /tmp/eval_err.XXXXXX)
|
||||
trap 'rm -f "$OUTF" "$ERRF"' EXIT
|
||||
|
||||
plain() { sed 's/\x1b\[[0-9;]*m//g'; }
|
||||
|
||||
# run SOURCE [EXTRA...] — sets STATUS, and fills OUTF/ERRF.
|
||||
STATUS=0
|
||||
run() {
|
||||
local src="$1"; shift
|
||||
"$KTEXT" --klammersets none -s "$src" -d "$@" >"$OUTF" 2>"$ERRF"
|
||||
STATUS=$?
|
||||
}
|
||||
|
||||
pass() { echo "${green}PASS${reset} $1"; PASS=$((PASS + 1)); }
|
||||
fail() { echo "${red}FAIL${reset} $1"; [ -n "$2" ] && echo " $2"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
echo "${bold}@eval tests${reset}"
|
||||
echo "==========="
|
||||
echo
|
||||
|
||||
echo "-- :shell, the ordinary case --"
|
||||
run '@eval :shell echo hello @'
|
||||
if [ $STATUS -eq 0 ] && [ "$(cat "$OUTF")" = "hello" ]; then
|
||||
pass " 1. the command's stdout becomes document text"
|
||||
else
|
||||
fail " 1. exit $STATUS, stdout [$(cat "$OUTF")]"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "-- stderr belongs to the command, not to the terminal --"
|
||||
# A signal death is not an error exit, and a crash must never read as a pass:
|
||||
# every case here checks the status numerically.
|
||||
run '@eval :shell echo OUT; echo NOISE >&2 @'
|
||||
if [ $STATUS -eq 0 ] && [ "$(cat "$OUTF")" = "OUT" ]; then
|
||||
pass " 2. stdout is the document; stderr is not in it"
|
||||
else
|
||||
fail " 2. exit $STATUS, stdout [$(cat "$OUTF")]"
|
||||
fi
|
||||
if [ ! -s "$ERRF" ]; then
|
||||
pass " 3. ... and nothing leaks to the terminal at the default verbosity"
|
||||
else
|
||||
fail " 3. stderr leaked: $(head -1 "$ERRF")"
|
||||
fi
|
||||
run '@eval :shell echo OUT; echo NOISE >&2 @' -v 1
|
||||
if grep -q "NOISE" "$ERRF"; then
|
||||
pass " 4. ... while -v 1 reports what the command said"
|
||||
else
|
||||
fail " 4. -v 1 did not report the command's stderr" "$(head -2 "$ERRF")"
|
||||
fi
|
||||
if grep -qi "stderr" "$ERRF"; then
|
||||
pass " 5. ... and says that is what it is"
|
||||
else
|
||||
fail " 5. the -v 1 report does not identify the stream"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "-- a failing command is an error, not silence --"
|
||||
run '@eval :shell exit 3 @'
|
||||
if [ $STATUS -ne 0 ] && [ $STATUS -lt 128 ]; then
|
||||
pass " 6. a nonzero exit status fails the run"
|
||||
else
|
||||
fail " 6. exit $STATUS (128+ would be a signal death, 0 a silent pass)"
|
||||
fi
|
||||
for want in "exit status 3" "The shell command failed"; do
|
||||
if grep -qF "$want" "$ERRF"; then
|
||||
pass " 7. the error says [$want]"
|
||||
else
|
||||
fail " 7. the error does not say [$want]" "$(plain < "$ERRF" | head -2)"
|
||||
fi
|
||||
done
|
||||
# What the command itself reported is the useful half of the diagnosis.
|
||||
run '@eval :shell echo WHY-IT-FAILED >&2; exit 1 @'
|
||||
if grep -qF "WHY-IT-FAILED" "$ERRF"; then
|
||||
pass " 8. ... and includes what the command wrote to stderr"
|
||||
else
|
||||
fail " 8. the command's own message was dropped" "$(plain < "$ERRF" | head -3)"
|
||||
fi
|
||||
# An error is category 2: stderr, and nothing on stdout to confuse a pipe.
|
||||
if [ ! -s "$OUTF" ]; then
|
||||
pass " 9. ... and leaves stdout empty"
|
||||
else
|
||||
fail " 9. stdout carried [$(head -c 60 "$OUTF")]"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "-- the escape hatch, because some commands exit nonzero on purpose --"
|
||||
# "grep" finding no match is the usual one. Strictness with an explicit way to
|
||||
# say "I meant that" is the same shape as the @cond predicate rule.
|
||||
run '@eval :shell exit 3 @'
|
||||
if grep -qF "|| true" "$ERRF"; then
|
||||
pass "10. the error names the way to say a nonzero status was intended"
|
||||
else
|
||||
fail "10. the error does not offer the remedy" "$(plain < "$ERRF" | head -3)"
|
||||
fi
|
||||
# NOT "exit N || true": exit terminates the shell before "||" is reached, so
|
||||
# that spelling cannot work and is not what the message suggests. A command
|
||||
# that merely RETURNS nonzero is the case the remedy is for.
|
||||
run '@eval :shell echo kept; grep -q zzz /dev/null || true @'
|
||||
if [ $STATUS -eq 0 ] && [ "$(cat "$OUTF")" = "kept" ]; then
|
||||
pass "11. ... and it works"
|
||||
else
|
||||
fail "11. exit $STATUS, stdout [$(cat "$OUTF")]"
|
||||
fi
|
||||
# The same command without the remedy is an error, or case 11 proves nothing.
|
||||
run '@eval :shell echo kept; grep -q zzz /dev/null @'
|
||||
if [ $STATUS -ne 0 ] && [ $STATUS -lt 128 ]; then
|
||||
pass "11a. ... and without it the same command fails"
|
||||
else
|
||||
fail "11a. exit $STATUS — expected a nonzero, non-signal exit"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "-- the command may contain its own pipeline --"
|
||||
# The redirection that captures stderr must not disturb the writer's command.
|
||||
run '@eval :shell echo one two three | tr " " "-" @'
|
||||
if [ $STATUS -eq 0 ] && [ "$(cat "$OUTF")" = "one-two-three" ]; then
|
||||
pass "12. a pipeline inside the command still works"
|
||||
else
|
||||
fail "12. exit $STATUS, stdout [$(cat "$OUTF")]"
|
||||
fi
|
||||
run '@eval :shell echo a > /dev/null; echo b @'
|
||||
if [ $STATUS -eq 0 ] && [ "$(cat "$OUTF")" = "b" ]; then
|
||||
pass "13. ... and so does a redirection of its own"
|
||||
else
|
||||
fail "13. exit $STATUS, stdout [$(cat "$OUTF")]"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "-- the other modes still work --"
|
||||
run '@eval 6*7 @'
|
||||
if [ $STATUS -eq 0 ] && [ "$(cat "$OUTF")" = "42" ]; then
|
||||
pass "14. a Python expression"
|
||||
else
|
||||
fail "14. exit $STATUS, stdout [$(cat "$OUTF")]"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "-- :haskell, the same contract --"
|
||||
# Skipped where GHC is absent: runghc is an optional dependency (the
|
||||
# akopra/klammertext:haskell image, or a local GHCup install), and a suite that
|
||||
# fails for its absence would be reporting the machine, not the code.
|
||||
if ! command -v runghc >/dev/null 2>&1; then
|
||||
echo "SKIP 15-18. :haskell (runghc not installed)"
|
||||
else
|
||||
# The defect: runghc ran with "2>&1", so on a SUCCESSFUL run everything the
|
||||
# program or GHC wrote to stderr was merged into the result and became part
|
||||
# of the document.
|
||||
HS='@eval :haskell import System.IO
|
||||
main = hPutStrLn stderr "HS-NOISE" >> putStrLn "HS-OUT" @'
|
||||
run "$HS"
|
||||
if [ $STATUS -eq 0 ] && [ "$(cat "$OUTF")" = "HS-OUT" ]; then
|
||||
pass "15. the program's stdout is the document; its stderr is not"
|
||||
else
|
||||
fail "15. exit $STATUS, stdout [$(cat "$OUTF")]"
|
||||
fi
|
||||
run "$HS" -v 1
|
||||
if grep -q "HS-NOISE" "$ERRF"; then
|
||||
pass "16. ... and -v 1 reports what it wrote to stderr"
|
||||
else
|
||||
fail "16. -v 1 did not report it" "$(plain < "$ERRF" | head -2)"
|
||||
fi
|
||||
# A compile error was ALREADY reported rather than swallowed -- the exit
|
||||
# status was checked -- so this pins behaviour that was right, and that the
|
||||
# detail now comes from the captured stderr rather than a merged stream.
|
||||
run '@eval :haskell main = putStrLn (1 + "x") @'
|
||||
if [ $STATUS -ne 0 ] && [ $STATUS -lt 128 ] && grep -qi "error" "$ERRF"; then
|
||||
pass "17. a compile error fails the run and shows what runghc said"
|
||||
else
|
||||
fail "17. exit $STATUS" "$(plain < "$ERRF" | head -3)"
|
||||
fi
|
||||
# A program that compiles, runs, and then exits nonzero is the other half.
|
||||
run '@eval :haskell import System.Exit
|
||||
main = putStrLn "partial" >> exitWith (ExitFailure 3) @'
|
||||
if [ $STATUS -ne 0 ] && grep -qF "exit status 3" "$ERRF"; then
|
||||
pass "18. a nonzero exit from the program itself is reported too"
|
||||
else
|
||||
fail "18. exit $STATUS" "$(plain < "$ERRF" | head -3)"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "==========="
|
||||
echo "Results: ${PASS} passed, ${FAIL} failed"
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
exit 0
|
||||
@@ -14,7 +14,7 @@
|
||||
# regrouping is announced on stderr.
|
||||
# - A leading "~/" expands to $HOME (shells do not expand a quoted tilde).
|
||||
#
|
||||
# Engine tier: uses -k none; no SKS.
|
||||
# Engine tier: uses --klammersets none; no SKS.
|
||||
#
|
||||
# Usage: ./filename_test.sh
|
||||
# Exit code: 0 if all tests pass, 1 otherwise.
|
||||
@@ -98,36 +98,49 @@ echo "======================="
|
||||
|
||||
check "1. quoted filename with a space is one file" \
|
||||
"ALPHA" "" \
|
||||
"my file.kt" -d -k none
|
||||
"my file.kt" -d --klammersets none
|
||||
|
||||
check "2. unquoted spaces rescued into an existing file (announced)" \
|
||||
# The regrouping is a DERIVED value -- the command completed a name the input
|
||||
# left ambiguous -- so since 2026-08-15 it is reported at "-v 1" and the
|
||||
# default run is silent. Silence is safe because a rescue only succeeds when
|
||||
# the joined name names an existing file: a mistyped name fails instead of
|
||||
# resolving to something else.
|
||||
check "2. unquoted spaces are rescued into an existing file" \
|
||||
"ALPHA" "" \
|
||||
my file.kt -d --klammersets none
|
||||
|
||||
check "2a. ... silently at the default verbosity" \
|
||||
"ALPHA" "" \
|
||||
my file.kt -d --klammersets none
|
||||
|
||||
check "2b. ... and reported at -v 1" \
|
||||
"ALPHA" "interpreting \"my file.kt\" as one filename" \
|
||||
my file.kt -d -k none
|
||||
my file.kt -d --klammersets none -v 1
|
||||
|
||||
check "3. standalone / separates a filename list" \
|
||||
"ALPHA
|
||||
BETA" "" \
|
||||
my file.kt / b.kt -d -k none
|
||||
my file.kt / b.kt -d --klammersets none
|
||||
|
||||
check "4. space in a directory component" \
|
||||
"GAMMA" "" \
|
||||
"my dir/c.kt" -d -k none
|
||||
"my dir/c.kt" -d --klammersets none
|
||||
|
||||
check "5. quoted name that exists is never split (b.kt also exists)" \
|
||||
"ALPHA" "" \
|
||||
"my file.kt" -d -k none
|
||||
"my file.kt" -d --klammersets none
|
||||
|
||||
HOME="$DIR" check "6. quoted ~/ expands to \$HOME inside ktext" \
|
||||
"BETA" "" \
|
||||
"~/b.kt" -d -k none
|
||||
"~/b.kt" -d --klammersets none
|
||||
|
||||
check "7. @read argument keeps its internal space" \
|
||||
"ALPHA" "" \
|
||||
-s '@read my file.kt @' -d -k none
|
||||
-s '@read my file.kt @' -d --klammersets none
|
||||
|
||||
check_error "8. unrescuable name is reported as written" \
|
||||
"no such.kt" \
|
||||
"no such.kt" -d -k none
|
||||
"no such.kt" -d --klammersets none
|
||||
|
||||
echo
|
||||
echo "======================="
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
# * a flag a user reaches for often gets a single letter (-k klammers,
|
||||
# -t targets, -c character codes, -i input); a more specialised topic gets
|
||||
# a multi-letter name (--argtypes, --katoms, --rewrite, --optionsets,
|
||||
# --coverage, --klammerset, --font);
|
||||
# --coverage, --klammersets, --font);
|
||||
# * -v says how much to show about the command's PROCESSING and never what
|
||||
# its RESULT contains. So the katom regex column is "--katoms full" and
|
||||
# the coverage file column is "--coverage all", not verbosity levels.
|
||||
@@ -34,11 +34,25 @@ reset=$'\033[0m'
|
||||
|
||||
plain() { 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
|
||||
|
||||
# shows NAME PATTERN CMD... — exits 0 and the output matches PATTERN.
|
||||
shows() {
|
||||
local name="$1" pattern="$2"; shift 2
|
||||
local out status
|
||||
out=$(timeout 60 "$@" 2>&1 | plain); status=$?
|
||||
out=$(limited "$@" 2>&1 | plain); status=$?
|
||||
if [ $status -gt 128 ]; then
|
||||
echo "${red}FAIL${reset} $name — died by signal $((status-128))"; FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
@@ -53,7 +67,7 @@ shows() {
|
||||
# absent NAME PATTERN CMD... — the output does NOT match PATTERN.
|
||||
absent() {
|
||||
local name="$1" pattern="$2"; shift 2
|
||||
if timeout 60 "$@" 2>&1 | plain | grep -Eq -- "$pattern"; then
|
||||
if limited "$@" 2>&1 | plain | grep -Eq -- "$pattern"; then
|
||||
echo "${red}FAIL${reset} $name — unexpected match [$pattern]"; FAIL=$((FAIL+1))
|
||||
else
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
@@ -93,17 +107,17 @@ absent "12. --coverage alone omits it" 'sks/table/table\.k' "$KDESC"
|
||||
# reason that has nothing to do with what it is testing. (It did: adopting
|
||||
# ".*" in the SKS made "All targets, declared" non-empty.)
|
||||
shows "12a. --coverage all shows an empty reporting category" \
|
||||
'All targets, declared \(0\)' "$KDESC" -i "$DIR/cycle.k" --coverage all
|
||||
'All targets, declared \(0\)' "$KDESC" --klammersets none -i "$DIR/cycle.k" --coverage all
|
||||
absent "12b. ... hidden without it" \
|
||||
'All targets, declared \(0\)' "$KDESC" -i "$DIR/cycle.k" --coverage
|
||||
'All targets, declared \(0\)' "$KDESC" --klammersets none -i "$DIR/cycle.k" --coverage
|
||||
absent "12c. an empty problem category stays hidden" \
|
||||
'Declared but never defined \(0\)' "$KDESC" -i "$DIR/cycle.k" --coverage all
|
||||
'Declared but never defined \(0\)' "$KDESC" --klammersets none -i "$DIR/cycle.k" --coverage all
|
||||
absent "13. -v adds no result detail" 'Regex' "$KDESC" --katoms -v 3
|
||||
|
||||
echo
|
||||
echo "-- -v is about processing only --"
|
||||
shows "14. -v names the input it read" 'input_filenames' "$KDESC" -t -v 1
|
||||
absent "15. ... and is silent without it" 'input_filenames' "$KDESC" -t
|
||||
shows "14. -v names the input it read" 'input_filenames' "$KDESC" -i "$DIR/clean.k" -t -v 1
|
||||
absent "15. ... and is silent without it" 'input_filenames' "$KDESC" -i "$DIR/clean.k" -t
|
||||
|
||||
echo
|
||||
echo "-- the klammer search --"
|
||||
@@ -126,13 +140,46 @@ shows "25. --katoms help explains" 'Katom commands' "$KDESC" --katom
|
||||
shows "26. an unknown coverage word" 'Unrecognized coverage command' "$KDESC" --coverage nonsense
|
||||
shows "27. an unknown katom word" 'Unrecognized katom command' "$KDESC" --katoms nonsense
|
||||
|
||||
echo
|
||||
echo "-- provenance: -i shows what the INPUT defines --"
|
||||
# A klammerset is loaded so the input can be ANALYSED against it, but it is
|
||||
# not what the user asked about. A designer wants the klammers this file
|
||||
# defines; a reader of an unfamiliar document wants the custom ones it
|
||||
# carries. So "-i" restricts every listing to definitions from outside the
|
||||
# loaded klammersets -- while the klammersets stay loaded.
|
||||
PROV=$(mktemp /tmp/kdesc_prov.XXXXXX.k)
|
||||
printf '@@shout.k s : Say it loudly @@\n@@shout.html :: <b>*s*</b> @@\n' > "$PROV"
|
||||
shows "32. -i lists the input's own klammer" '@shout' "$KDESC" -i "$PROV" -k
|
||||
absent "33. ... and not the klammerset's" '@table' "$KDESC" -i "$PROV" -k
|
||||
shows "34. without -i the klammerset is shown" '@table' "$KDESC" -k
|
||||
# The klammerset is still LOADED: a coverage derived from a klammer it
|
||||
# supplies proves the analysis saw it even though the report does not.
|
||||
DERIVE=$(mktemp /tmp/kdesc_derive.XXXXXX.k)
|
||||
printf '@@wave.k s : Wave @@\n@@wave :: @i *s* @ @@\n' > "$DERIVE"
|
||||
shows "35. coverage is derived through the unlisted klammerset" \
|
||||
'@wave +html pdf tex txt +from @i' "$KDESC" -i "$DERIVE" --coverage
|
||||
absent "36. ... and the klammerset is not reported" '@table' "$KDESC" -i "$DERIVE" --coverage
|
||||
# A klammer the input REDEFINES came from the klammerset, but the input
|
||||
# changed it -- which is exactly what a reader needs to know.
|
||||
REDEF=$(mktemp /tmp/kdesc_redef.XXXXXX.k)
|
||||
printf '@@i.html ::: <em class="mine">*text*</em> @@\n' > "$REDEF"
|
||||
shows "37. a redefined klammer is the input's too" '@i' "$KDESC" -i "$REDEF" -k
|
||||
# An input that declares its own klammerset is still the input.
|
||||
DECL=$(mktemp -d /tmp/kdesc_decl.XXXXXX)
|
||||
mkdir -p "$DECL/own"
|
||||
printf '@@@klammerset own | A designer set @@@\n@@yell.k s : Yell @@\n@@yell.html :: <b>*s*</b> @@\n' \
|
||||
> "$DECL/own/own.k"
|
||||
shows "38. an input declaring a klammerset is not filtered out" \
|
||||
'@yell' "$KDESC" -i "$DECL/own/own.k" -k
|
||||
rm -rf "$PROV" "$DERIVE" "$REDEF" "$DECL"
|
||||
|
||||
echo
|
||||
echo "-- the usage text --"
|
||||
shows "28. -k shows its optional argument" '\-k \[<text>\]' "$KDESC"
|
||||
shows "29. -i shows its filename" '\-i <filename>' "$KDESC"
|
||||
# Ordered by likely use: the single letters come before the long names.
|
||||
# Line numbers, not a multi-line pattern -- grep is line-oriented.
|
||||
usage=$(timeout 60 "$KDESC" 2>&1 | plain)
|
||||
usage=$(limited "$KDESC" 2>&1 | plain)
|
||||
k_line=$(printf '%s\n' "$usage" | grep -n -- '-k \[<text>\]' | head -1 | cut -d: -f1)
|
||||
katoms_line=$(printf '%s\n' "$usage" | grep -n -- '--katoms' | head -1 | cut -d: -f1)
|
||||
if [ -n "$k_line" ] && [ -n "$katoms_line" ] && [ "$k_line" -lt "$katoms_line" ]; then
|
||||
@@ -144,7 +191,7 @@ fi
|
||||
|
||||
echo
|
||||
echo "-- a no-result search is not an error --"
|
||||
timeout 60 "$KDESC" -k zzqq >/dev/null 2>&1
|
||||
limited "$KDESC" -k zzqq >/dev/null 2>&1
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "${green}PASS${reset} 31. finding nothing exits 0"; PASS=$((PASS+1))
|
||||
else
|
||||
|
||||
311
tst/kdiag_test.sh
Executable file
311
tst/kdiag_test.sh
Executable file
@@ -0,0 +1,311 @@
|
||||
#!/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
|
||||
12
tst/klammerset/base/base.k
Normal file
12
tst/klammerset/base/base.k
Normal file
@@ -0,0 +1,12 @@
|
||||
# Engine-tier fixture: the base of the klammerset COMBINING tests. It owns the
|
||||
# target and the ".k" declaration that the two "house" sets override, so each
|
||||
# of them can require it and neither has to be loaded first on the command
|
||||
# line -- which is what makes command-line ORDER the only thing under test.
|
||||
#
|
||||
# The definitions sit in a :files entry rather than in this declaring file,
|
||||
# which is NOT a style choice: the already-loaded guard skips a repeated
|
||||
# declaration's :files, but the declaring file itself is read again every time
|
||||
# a :requires names it. With the "@@@target fx" written here, requiring
|
||||
# "base" from two sets reads it twice and the second read is a fatal "Target
|
||||
# fx is already defined". See case 21c, which pins that.
|
||||
@@@klammerset base | Base set for combining tests :date 2026-08-15 :files defs.k @@@
|
||||
4
tst/klammerset/base/defs.k
Normal file
4
tst/klammerset/base/defs.k
Normal file
@@ -0,0 +1,4 @@
|
||||
# The base set's definitions. See base.k for why they are not in it.
|
||||
@@@target fx | Combining fixture target @@@
|
||||
@@who.k : Which klammerset supplied this definition @@
|
||||
@@who.fx :: BASE @@
|
||||
@@ -1,11 +0,0 @@
|
||||
# Engine-tier fixture: a klammerset with metadata, a dependency, an ordered
|
||||
# file list (klammers.k uses the target defined in base.k), and a trailing
|
||||
# definition that must load after the files (program order).
|
||||
@@@klammerset kit | Engine-test klammerset
|
||||
:name Klammerset Integration Test
|
||||
:author Klammertext tests
|
||||
:date 2026-07-30
|
||||
:requires util.k
|
||||
:files base.k / klammers.k
|
||||
@@@
|
||||
@@after : AFTER @@
|
||||
@@ -1 +0,0 @@
|
||||
@@@klammerset kit | Duplicate declaration of kit :files dup_only.k @@@
|
||||
9
tst/klammerset/housea/housea.k
Normal file
9
tst/klammerset/housea/housea.k
Normal file
@@ -0,0 +1,9 @@
|
||||
# One of two independent sets of house overrides. Both require "base" (loaded
|
||||
# once, whichever asks first) and both override the same klammer with ":::", so
|
||||
# the one written LAST on the command line is the one that wins. Its own
|
||||
# klammer @a_only shows that the sets share one flat namespace rather than
|
||||
# each keeping its klammers to itself.
|
||||
@@@klammerset housea | House overrides A :requires base :date 2026-08-15 @@@
|
||||
@@who.fx ::: HOUSE-A @@
|
||||
@@a_only.k : A klammer only set A defines @@
|
||||
@@a_only.fx :: ONLY-A @@
|
||||
5
tst/klammerset/houseb/houseb.k
Normal file
5
tst/klammerset/houseb/houseb.k
Normal file
@@ -0,0 +1,5 @@
|
||||
# The mirror image of housea; see the comment there.
|
||||
@@@klammerset houseb | House overrides B :requires base :date 2026-08-15 @@@
|
||||
@@who.fx ::: HOUSE-B @@
|
||||
@@b_only.k : A klammer only set B defines @@
|
||||
@@b_only.fx :: ONLY-B @@
|
||||
18
tst/klammerset/kit/kit.k
Normal file
18
tst/klammerset/kit/kit.k
Normal file
@@ -0,0 +1,18 @@
|
||||
# Engine-tier fixture: a klammerset with metadata, a dependency, an ordered
|
||||
# file list (klammers.k uses the target defined in base.k), and a trailing
|
||||
# definition that must load after the files (program order).
|
||||
#
|
||||
# Laid out as <symbol>/<symbol>.k, which is what a symbol names: since
|
||||
# 2026-08-14 the command line takes symbols only, so a klammerset that
|
||||
# cannot be named by one cannot be loaded at all. ":requires util" is a
|
||||
# SYMBOL, resolved on the search path (this directory is not util's parent,
|
||||
# so it is found at stage 2, KLAMMERTEXT_KLAMMERSETS); ":files" entries are
|
||||
# filenames, resolved against this file's directory.
|
||||
@@@klammerset kit | Engine-test klammerset
|
||||
:name Klammerset Integration Test
|
||||
:author Klammertext tests
|
||||
:date 2026-07-30
|
||||
:requires util
|
||||
:files base.k / klammers.k
|
||||
@@@
|
||||
@@after : AFTER @@
|
||||
2
tst/klammerset/needs_a/needs_a.k
Normal file
2
tst/klammerset/needs_a/needs_a.k
Normal file
@@ -0,0 +1,2 @@
|
||||
# Requires selfdef; paired with needs_b to make the diamond of case 21c.
|
||||
@@@klammerset needs_a | Requires selfdef :requires selfdef :date 2026-08-15 @@@
|
||||
2
tst/klammerset/needs_b/needs_b.k
Normal file
2
tst/klammerset/needs_b/needs_b.k
Normal file
@@ -0,0 +1,2 @@
|
||||
# Requires selfdef; paired with needs_a to make the diamond of case 21c.
|
||||
@@@klammerset needs_b | Requires selfdef :requires selfdef :date 2026-08-15 @@@
|
||||
8
tst/klammerset/selfdef/selfdef.k
Normal file
8
tst/klammerset/selfdef/selfdef.k
Normal file
@@ -0,0 +1,8 @@
|
||||
# A klammerset whose declaring file carries its own definitions (the
|
||||
# documented alternative to a :text option: program order plays that role).
|
||||
# Requiring it from two different sets reads this file twice, and the second
|
||||
# read of the @@@target is fatal. Case 21c pins that defect; when it is
|
||||
# fixed, that case fails and must be rewritten.
|
||||
@@@klammerset selfdef | Definitions in the declaring file :date 2026-08-15 @@@
|
||||
@@@target fy | Self-defining fixture target @@@
|
||||
@@sd.fy : SELFDEF @@
|
||||
6
tst/klammerset/sp/sp.k
Normal file
6
tst/klammerset/sp/sp.k
Normal file
@@ -0,0 +1,6 @@
|
||||
# The target and @greet come from kit, required by symbol; this set adds
|
||||
# only the file whose name contains a space (standalone "/" separator).
|
||||
@@@klammerset sp | Spacey filename in the list
|
||||
:requires kit
|
||||
:files space name.k
|
||||
@@@
|
||||
@@ -1 +0,0 @@
|
||||
@@@klammerset sp | Spacey filename in the list :files base.k / space name.k @@@
|
||||
6
tst/klammerset/wrap/dup.k
Normal file
6
tst/klammerset/wrap/dup.k
Normal file
@@ -0,0 +1,6 @@
|
||||
# A declaration of the symbol "kit" from a file that is NOT kit/kit.k.
|
||||
# Since 2026-08-15 that is an ERROR: a klammerset that can be named is
|
||||
# identified by where it is, so a symbol names one file and one file declares
|
||||
# one symbol. This fixture exists to prove the error; it formerly proved that
|
||||
# such a duplicate was tolerated and skipped.
|
||||
@@@klammerset kit | Duplicate declaration of kit :files dup_only.k @@@
|
||||
3
tst/klammerset/wrap/wrap.k
Normal file
3
tst/klammerset/wrap/wrap.k
Normal file
@@ -0,0 +1,3 @@
|
||||
# Requires "kit" twice over: once by symbol, once through dup.k, which
|
||||
# re-declares it. Loading "wrap" is therefore an error now -- see dup.k.
|
||||
@@@klammerset wrap | Wrapper requiring kit twice :requires kit / dup.k @@@
|
||||
@@ -1 +0,0 @@
|
||||
@@@klammerset wrap | Wrapper requiring kit twice :requires decl.k / decl2.k @@@
|
||||
@@ -23,6 +23,14 @@ KTEXT=ktext
|
||||
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
|
||||
FIX=$K/tst/klammerset
|
||||
|
||||
# The fixtures are laid out as <symbol>/<symbol>.k under $FIX, because since
|
||||
# 2026-08-14 the command line takes klammerset SYMBOLS only -- a set that
|
||||
# cannot be named by one cannot be loaded. Putting $FIX on the search path is
|
||||
# how the suite makes its own fixtures nameable, and it also exercises stage 2
|
||||
# (KLAMMERTEXT_KLAMMERSETS) for both the command line and ":requires".
|
||||
# Tests that need a different path set it themselves on the command.
|
||||
export KLAMMERTEXT_KLAMMERSETS=$FIX
|
||||
|
||||
red=$'\033[31m'
|
||||
green=$'\033[32m'
|
||||
bold=$'\033[1m'
|
||||
@@ -127,23 +135,23 @@ echo
|
||||
# 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
|
||||
--klammersets kit -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
|
||||
--klammersets kit -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
|
||||
--klammersets kit -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 )
|
||||
output=$( (cd /tmp && "$KTEXT" --klammersets kit -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))
|
||||
@@ -157,45 +165,83 @@ 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
|
||||
--klammersets sp -s '@spacey@' -t fix
|
||||
|
||||
# --- The already-loaded guard ------------------------------------------------
|
||||
# --- A klammerset is identified by where it is -------------------------------
|
||||
#
|
||||
# Symbol X must be declared in X/X.k (Andy, 2026-08-15). The convention already
|
||||
# governed symbol RESOLUTION; requiring it of the DECLARATION makes the symbol a
|
||||
# function of the path, which is what lets the already-loaded guard run before a
|
||||
# file is opened (cases 23-23b below) instead of after it has been re-executed.
|
||||
#
|
||||
# It also continues the reasoning behind symbols-only on the command line: if
|
||||
# every klammerset is reachable by symbol the set of all of them is ENUMERABLE;
|
||||
# if every declaration is in X/X.k each one is also IDENTIFIABLE from where it
|
||||
# sits, so a symbol names one file and one file declares one symbol.
|
||||
#
|
||||
# "wrap" is the fixture that violates it: wrap/dup.k declares the symbol "kit".
|
||||
# It formerly tested that such a duplicate was tolerated and SKIPPED; the
|
||||
# duplicate is now impossible instead, which is the stronger guarantee.
|
||||
check_fails "6. a declaration outside X/X.k is an error" \
|
||||
'must be declared in a file named "kit/kit.k"' \
|
||||
--klammersets wrap -s '@greet Again @' -t fix
|
||||
check_fails "6a. ... and the error names the file it is actually in" \
|
||||
'dup.k' \
|
||||
--klammersets wrap -s '@greet Again @' -t fix
|
||||
|
||||
# 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
|
||||
# 7. EXEMPT: a document may declare a klammerset -- that is how a designer
|
||||
# writes one, and kdesc's provenance filter depends on it. Such a set is
|
||||
# local to the document: nothing can ":requires" it, so it has no identity to
|
||||
# protect and needs no file named after it.
|
||||
check_eq "7. a klammerset declared in a document is exempt" \
|
||||
"ok" \
|
||||
--klammersets none -s '@@@klammerset mine | Local to this document @@@ ok'
|
||||
|
||||
# --- 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
|
||||
# The machine state moved from "ktext -m" to "kdiag --machine" in the
|
||||
# 2026-08-14 argument redesign: the Machine's internals are the programmer's
|
||||
# question, and kdiag is the programmer's command. kdiag's input is optional,
|
||||
# so the state can be shown without a document at all.
|
||||
kdiag_contains() {
|
||||
local test_name="$1" expected="$2"; shift 2
|
||||
local output
|
||||
output=$(kdiag "$@" 2>/dev/null)
|
||||
if printf '%s' "$output" | grep -qF -- "$expected"; then
|
||||
echo "${green}PASS${reset} $test_name"; PASS=$((PASS + 1))
|
||||
else
|
||||
echo "${red}FAIL${reset} $test_name"
|
||||
echo " expected to contain: [$expected]"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
check_contains "9. -m shows the required klammerset too" \
|
||||
# 8. --machine lists the registered klammersets. kdiag has no --klammersets:
|
||||
# a klammerset reaches it the way anything else does, by being read in the
|
||||
# input. That is deliberate -- the symbol mechanism is a higher-level
|
||||
# convenience, and kdiag must still work when symbol resolution is what
|
||||
# broke. (The :requires inside kit.k still uses a symbol, resolved by the
|
||||
# Machine, so this exercises both paths at once.)
|
||||
kdiag_contains "8. --machine shows the klammerset symbol" \
|
||||
"kit" \
|
||||
"@read $FIX/kit/kit.k @" --process --machine
|
||||
|
||||
kdiag_contains "9. --machine shows the required klammerset too" \
|
||||
"Utility klammers for engine tests" \
|
||||
-k "$FIX/decl.k" -s 'x' -t fix -m
|
||||
"@read $FIX/kit/kit.k @" --process --machine
|
||||
|
||||
# --- 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
|
||||
--klammersets broken -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
|
||||
--klammersets none -s '@@@klammerset 9bad | Bad symbol @@@' -d
|
||||
|
||||
# --- The search path (symbol -> <dir>/<symbol>/<symbol>.k) -------------------
|
||||
|
||||
@@ -217,10 +263,10 @@ printf '@@@klammerset locset | Shadow candidate @@@\n@@@target fixL | Fixture @@
|
||||
# 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
|
||||
"$DOCDIR/doc.kt" --klammersets 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 )
|
||||
output=$( (cd "$DOCDIR" && "$KTEXT" -s '@local_k@' --klammersets 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))
|
||||
@@ -231,7 +277,7 @@ else
|
||||
fi
|
||||
|
||||
# 14. Stage 2: the KLAMMERTEXT_KLAMMERSETS directories.
|
||||
output=$(KLAMMERTEXT_KLAMMERSETS=$ENVDIR "$KTEXT" -s '@env_k@' -k envset -t fixE 2>/dev/null | trim)
|
||||
output=$(KLAMMERTEXT_KLAMMERSETS=$ENVDIR "$KTEXT" -s '@env_k@' --klammersets envset -t fixE 2>/dev/null | trim)
|
||||
if [ "$output" = "ENV" ]; then
|
||||
echo "${green}PASS${reset} 14. symbol resolves in KLAMMERTEXT_KLAMMERSETS"
|
||||
PASS=$((PASS + 1))
|
||||
@@ -242,7 +288,7 @@ else
|
||||
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)
|
||||
output=$(KLAMMERTEXT_KLAMMERSETS=$ENVDIR "$KTEXT" "$DOCDIR/doc.kt" --klammersets 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))
|
||||
@@ -257,26 +303,170 @@ fi
|
||||
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
|
||||
output=$(KLAMMERTEXT_KLAMMERSETS=$DOCDIR "$KTEXT" --klammersets kit2 \
|
||||
-s '@local_k@' -t fixL 2>/dev/null | trim)
|
||||
if [ "$output" = "LOCAL" ]; then
|
||||
echo "${green}PASS${reset} 16. :requires accepts a symbol (declaring-dir stage)"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo "${red}FAIL${reset} 16. :requires accepts a symbol (declaring-dir stage)"
|
||||
echo " expected: [LOCAL] got: [$output]"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
# 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)
|
||||
output=$(kdesc --klammersets 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"
|
||||
echo "${green}PASS${reset} 17. kdesc --klammersets lists sks from KLAMMERTEXT_HOME"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo "${red}FAIL${reset} 17. kdesc --klammerset lists sks from KLAMMERTEXT_HOME"
|
||||
echo "${red}FAIL${reset} 17. kdesc --klammersets 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" \
|
||||
# 18. An unknown symbol is a clean error, and it names both the convention and
|
||||
# the directories searched -- without those a user cannot tell whether the
|
||||
# name or the location is wrong. This is the failure mode the symbol-only
|
||||
# rule makes common: there is no longer a pathname to fall back on.
|
||||
check_fails "18. unknown symbol is a clean error" \
|
||||
"was not found" \
|
||||
-s 'x' -k nosuchset -d
|
||||
-s 'x' --klammersets nosuchset -d
|
||||
check_fails "18a. ... naming the <symbol>/<symbol>.k convention" \
|
||||
"x/x.k" \
|
||||
-s 'x' --klammersets nosuchset -d
|
||||
check_fails "18b. ... and the directories searched" \
|
||||
"$FIX" \
|
||||
-s 'x' --klammersets nosuchset -d
|
||||
|
||||
# 19. A pathname is no longer accepted: only symbols resolve. The error must
|
||||
# say so rather than reporting a missing file.
|
||||
check_fails "19. a pathname is rejected as a symbol" \
|
||||
"not found" \
|
||||
-s 'x' --klammersets "$FIX/kit/kit.k" -d
|
||||
|
||||
# 20. One bad symbol among good ones fails the whole load rather than
|
||||
# silently loading the rest.
|
||||
check_fails "20. a bad symbol among good ones still fails" \
|
||||
"was not found" \
|
||||
-s 'x' --klammersets kit nosuchset -t fix -d
|
||||
|
||||
# --- Several klammersets combine ---------------------------------------------
|
||||
#
|
||||
# The headline claim of "--klammersets" (plural), stated in the ktext usage and
|
||||
# in "kdesc --klammersets help": the symbols are a LIST, loaded in the order
|
||||
# given, and the klammers of all of them share ONE flat namespace -- membership
|
||||
# is provenance, not containment. Where two sets define the same klammer the
|
||||
# definition modes decide, so a set of house overrides is loaded after the set
|
||||
# it adjusts and a set of defaults before it.
|
||||
#
|
||||
# The fixtures: "base" declares the target and the ".k", "housea" and "houseb"
|
||||
# each require base and each override @who with ":::". Both requiring base is
|
||||
# what lets either be written first, so command-line order is the only variable.
|
||||
|
||||
# 21. One namespace: each set's own klammers are available whatever the order.
|
||||
check_eq "21. combined sets share one namespace" \
|
||||
"ONLY-A ONLY-B" \
|
||||
--klammersets housea houseb -s '@a_only@ @b_only@' -t fx
|
||||
check_eq "21a. ... in either order" \
|
||||
"ONLY-A ONLY-B" \
|
||||
--klammersets houseb housea -s '@a_only@ @b_only@' -t fx
|
||||
|
||||
# 22. Order decides: two ":::" overrides of the same klammer, so the set
|
||||
# written LAST is the one whose definition survives.
|
||||
check_eq "22. the last set written wins an override" \
|
||||
"HOUSE-B" \
|
||||
--klammersets housea houseb -s '@who@' -t fx
|
||||
check_eq "22a. ... and reversing the order reverses the outcome" \
|
||||
"HOUSE-A" \
|
||||
--klammersets houseb housea -s '@who@' -t fx
|
||||
|
||||
# 23. :requires is loaded once even though both sets ask for it -- the
|
||||
# already-loaded guard -- so the base definition is there exactly once.
|
||||
check_eq "23. a set required by both is loaded once" \
|
||||
"BASE" \
|
||||
--klammersets base -s '@who@' -t fx
|
||||
|
||||
# 23a. A DIAMOND: two sets requiring a third. "needs_a" and "needs_b" both
|
||||
# require "selfdef", whose declaring file holds its own @@@target -- which
|
||||
# is legal and normal, since there is deliberately no :text option and the
|
||||
# declaring file's content plays that role.
|
||||
#
|
||||
# This failed until 2026-08-15 with "Target fy is already defined",
|
||||
# pointing at a line the author wrote once. The already-loaded guard sat
|
||||
# in Klammerset_registry::add, which runs only after a file has been read
|
||||
# and its declaration reached -- by which time the second read had
|
||||
# re-executed the declaring file's own definitions. The guard now runs
|
||||
# BEFORE the read, which is possible only because X is declared in X/X.k
|
||||
# and the symbol is therefore known from the path.
|
||||
check_eq "23a. a diamond :requires loads the shared set once" \
|
||||
"SELFDEF" \
|
||||
--klammersets needs_a needs_b -s '@sd@' -t fy
|
||||
check_eq "23b. ... and requiring it once is unchanged" \
|
||||
"SELFDEF" \
|
||||
--klammersets needs_a -s '@sd@' -t fy
|
||||
check_eq "23c. ... as is naming the same set twice on the command line" \
|
||||
"SELFDEF" \
|
||||
--klammersets selfdef selfdef -s '@sd@' -t fy
|
||||
|
||||
# 23d. The search path is FROZEN for the run. A path decides WHICH FILE a
|
||||
# symbol means -- identity, not value -- so if a document could extend it
|
||||
# mid-run, "which klammerset is X" would depend on evaluation order and
|
||||
# "kdesc --klammersets" could not be a complete answer. It could:
|
||||
# the embedded Python shares the process, so an @eval doing os.environ
|
||||
# changed what a later getenv returned. The environment is now read once.
|
||||
LATE=$(mktemp -d /tmp/klammerset_late.XXXXXX)
|
||||
mkdir -p "$LATE/late"
|
||||
printf '@@@klammerset late | Appeared mid-run @@@\n@@@target lt | T @@@\n@@l.lt : LATE @@\n' \
|
||||
> "$LATE/late/late.k"
|
||||
# The @eval still sets the variable -- what changed is that resolution no
|
||||
# longer consults it -- so the symbol must remain unfindable.
|
||||
out=$(KLAMMERTEXT_KLAMMERSETS=$FIX "$KTEXT" --klammersets none -d \
|
||||
-s "@eval os.environ.__setitem__('KLAMMERTEXT_KLAMMERSETS','$LATE') or '' @" 2>&1)
|
||||
if printf '%s' "$out" | grep -q "was not found" || \
|
||||
! KLAMMERTEXT_KLAMMERSETS=$FIX "$KTEXT" --klammersets late -s 'x' -d >/dev/null 2>&1; then
|
||||
echo "${green}PASS${reset} 23d. an @eval cannot extend the klammerset search path"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo "${red}FAIL${reset} 23d. the search path moved during the run"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
rm -rf "$LATE"
|
||||
|
||||
# --- "none" is the absence of a klammerset, not the name of one ---------------
|
||||
#
|
||||
# It therefore says nothing about what should be loaded alongside it. Written
|
||||
# first it used to discard the rest of the list silently -- the user asked for
|
||||
# a set, got none, and was told only that some target was undefined; written
|
||||
# last it was looked up as a symbol and reported missing. Neither reading is
|
||||
# guessed at now: "none" is accepted only alone.
|
||||
|
||||
# 24. "none" alone still loads nothing, which is what it is for.
|
||||
check_eq "24. none alone loads no klammerset" \
|
||||
"x" \
|
||||
--klammersets none -s 'x' -d
|
||||
|
||||
check_fails "25. none before another symbol is an error" \
|
||||
"cannot be combined" \
|
||||
--klammersets none kit -s 'x' -t fix -d
|
||||
check_fails "25a. none after another symbol is an error too" \
|
||||
"cannot be combined" \
|
||||
--klammersets kit none -s 'x' -t fix -d
|
||||
|
||||
# 26. The rule belongs to load_klammersets(), the single loader all three
|
||||
# commands share -- so kdesc obeys it as well. It did not: kdesc decided
|
||||
# what "none" meant a second time, and so accepted "none sks" and loaded
|
||||
# neither. Deciding it twice is how the two commands came to disagree.
|
||||
out=$(kdesc --klammersets none sks -k table 2>&1); status=$?
|
||||
if [ $status -ne 0 ] && printf '%s' "$out" | grep -qF "cannot be combined"; then
|
||||
echo "${green}PASS${reset} 26. kdesc applies the same none rule"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo "${red}FAIL${reset} 26. kdesc applies the same none rule — exit $status"
|
||||
echo " got: $(printf '%s' "$out" | head -2)"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "======================="
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
# files read earlier. A klammer DEFINED in a read file is applied later and
|
||||
# always sees its own directory — the realistic case, tested below.
|
||||
#
|
||||
# Engine tier: -k none, fixtures created inline in a temp tree, no SKS.
|
||||
# Engine tier: --klammersets none, fixtures created inline in a temp tree, no SKS.
|
||||
# Usage: ./modulepath_test.sh (ktext on PATH)
|
||||
|
||||
PASS=0
|
||||
@@ -50,7 +50,7 @@ printf '@eval nosuch_module_xyz.f() @\n' > "$T/setb/missing.kt"
|
||||
check() { # check NAME EXPECTED ARGS...
|
||||
local name="$1" expected="$2"; shift 2
|
||||
local out
|
||||
out=$(ktext "$@" -k none -d 2>/dev/null)
|
||||
out=$(ktext "$@" --klammersets none -d 2>/dev/null)
|
||||
if [ "$out" = "$expected" ]; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS + 1))
|
||||
else
|
||||
@@ -75,7 +75,7 @@ check " 2. the same, input given as a relative path" \
|
||||
"../setb/doc.kt"
|
||||
|
||||
name=" 3. an unknown module still reports cleanly"
|
||||
err=$(ktext "$T/setb/missing.kt" -k none -d 2>&1 >/dev/null)
|
||||
err=$(ktext "$T/setb/missing.kt" --klammersets none -d 2>&1 >/dev/null)
|
||||
case "$err" in
|
||||
*'Cannot import module "nosuch_module_xyz"'*)
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS + 1)) ;;
|
||||
@@ -111,7 +111,7 @@ check " 7. a :cwd directory containing spaces" \
|
||||
|
||||
name=" 8. a nonexistent :cwd is a clean argument error"
|
||||
printf '@eval :cwd /nonexistent_kt_dir :shell true @\n' > "$T/setb/bad.kt"
|
||||
err=$(ktext "$T/setb/bad.kt" -k none -d 2>&1 >/dev/null)
|
||||
err=$(ktext "$T/setb/bad.kt" --klammersets none -d 2>&1 >/dev/null)
|
||||
case "$err" in
|
||||
*'The :cwd directory does not exist'*)
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS + 1)) ;;
|
||||
|
||||
@@ -50,7 +50,7 @@ PRELUDE='@@@target fix | a fixture target @@@
|
||||
check_eq() {
|
||||
local name="$1" expected="$2" source="$3" target="${4:-fix}"
|
||||
local output status
|
||||
output=$("$KTEXT" -k none -s "$PRELUDE
|
||||
output=$("$KTEXT" --klammersets none -s "$PRELUDE
|
||||
$source" -t "$target" -d 2>&1)
|
||||
status=$?
|
||||
output=$(printf '%s' "$output" | tr -d '\n' | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
|
||||
@@ -76,7 +76,7 @@ $source" -t "$target" -d 2>&1)
|
||||
check_fails() {
|
||||
local name="$1" needle="$2" source="$3"
|
||||
local output status
|
||||
output=$("$KTEXT" -k none -s "$PRELUDE
|
||||
output=$("$KTEXT" --klammersets none -s "$PRELUDE
|
||||
$source" -t fix -d 2>&1)
|
||||
status=$?
|
||||
if [ $status -eq 0 ]; then
|
||||
@@ -98,11 +98,23 @@ $source" -t fix -d 2>&1)
|
||||
# check_warns NAME SUBSTRING SOURCE — SOURCE must be accepted, and its
|
||||
# combined output must contain SUBSTRING (used where a warning is expected
|
||||
# beside the result, so an exact comparison would test the warning's wording).
|
||||
# Since 2026-08-15 the override notice is reported at "-v 1" rather than
|
||||
# warned about: ":::" exists to override, so the notice fired on the sanctioned
|
||||
# use of a feature (see tst/deftype_test.sh's check_warn for the reasoning).
|
||||
# The default run must be SILENT and the notice must appear at "-v 1".
|
||||
check_warns() {
|
||||
local name="$1" needle="$2" source="$3"
|
||||
local output status
|
||||
output=$("$KTEXT" -k none -s "$PRELUDE
|
||||
$source" -t fix -d 2>&1)
|
||||
local output status quiet
|
||||
quiet=$("$KTEXT" --klammersets none -s "$PRELUDE
|
||||
$source" -t fix -d 2>&1 >/dev/null)
|
||||
if [ -n "$quiet" ]; then
|
||||
echo "${red}FAIL${reset} $name — the default run was not silent"
|
||||
echo " got: $(printf '%s' "$quiet" | head -1)"
|
||||
FAIL=$((FAIL + 1))
|
||||
return
|
||||
fi
|
||||
output=$("$KTEXT" --klammersets none -s "$PRELUDE
|
||||
$source" -t fix -d -v 1 2>&1)
|
||||
status=$?
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "${red}FAIL${reset} $name — ktext exited $status"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# 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
|
||||
# These are engine tests: no klammer set is loaded (--klammersets none) and every klammer
|
||||
# used is defined inline as a fixture.
|
||||
#
|
||||
# Usage: ./recursion_test.sh
|
||||
@@ -100,29 +100,29 @@ echo
|
||||
|
||||
check_error " 1. direct self-recursion is caught" \
|
||||
"does not terminate" \
|
||||
-k none -s '@@f : x @f@ @@ @f@' -d
|
||||
--klammersets 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
|
||||
--klammersets 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
|
||||
--klammersets 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
|
||||
--klammersets none -s '@@w t : < *t* > @@ @@r : @w @r@ @ @@ @r@' -d
|
||||
|
||||
# --- Terminating nesting is untouched ---
|
||||
|
||||
check_eq " 5. deep but finite nesting still reduces" \
|
||||
"<<<<<x>>>>>" \
|
||||
-k none -s '@@w t : <*t*> @@ @w @w @w @w @w x @ @ @ @ @' -d
|
||||
--klammersets 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
|
||||
--klammersets 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" ---
|
||||
#
|
||||
@@ -131,7 +131,7 @@ check_eq " 6. a chain of klammers generating klammers reduces" \
|
||||
|
||||
check_eq " 7. a klammer with an empty body reduces" \
|
||||
"a b" \
|
||||
-k none -s '@@nothing : @@ a @nothing@ b' -d
|
||||
--klammersets none -s '@@nothing : @@ a @nothing@ b' -d
|
||||
|
||||
echo
|
||||
echo "============================="
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
# target a "::" instance with no parameter list of its own. That is the
|
||||
# migration from repeating an argument list per target to declaring it once.
|
||||
#
|
||||
# These are engine tests, so they use -k none and define their own targets
|
||||
# These are engine tests, so they use --klammersets none and define their own targets
|
||||
# inline: a target is a Machine construct (@@@target), not owned by any
|
||||
# klammer set.
|
||||
#
|
||||
@@ -46,7 +46,7 @@ TARGETS='@@@target ta | Target A @@@
|
||||
# accepted NAME SRC — the definitions are consistent and the klammer applies.
|
||||
accepted() {
|
||||
local name="$1" src="$2" out
|
||||
out=$("$KTEXT" -k none -s "$TARGETS$src" -t ta -d 2>&1)
|
||||
out=$("$KTEXT" --klammersets none -s "$TARGETS$src" -t ta -d 2>&1)
|
||||
if echo "$out" | grep -qi "error"; then
|
||||
fail "$name" "no error" "$(echo "$out" | grep -i -A1 error | tail -1)"
|
||||
else
|
||||
@@ -57,7 +57,7 @@ accepted() {
|
||||
# rejected NAME SRC PATTERN — the drift is caught, and the message says how.
|
||||
rejected() {
|
||||
local name="$1" src="$2" want="$3" out
|
||||
out=$("$KTEXT" -k none -s "$TARGETS$src" -t ta -d 2>&1)
|
||||
out=$("$KTEXT" --klammersets none -s "$TARGETS$src" -t ta -d 2>&1)
|
||||
if ! echo "$out" | grep -qi "error"; then
|
||||
fail "$name" "a definition error" "accepted"
|
||||
elif ! echo "$out" | grep -qF "$want"; then
|
||||
@@ -102,7 +102,7 @@ echo "-- the message must show HOW they differ, not just where"
|
||||
# With more than two targets the designer would otherwise have to diff the
|
||||
# definitions by hand; each target's own signature is listed beside its name.
|
||||
name=" 6. each target's signature is shown"
|
||||
out=$("$KTEXT" -k none -s "$TARGETS"'@@k6.ta s :one : [*s*] @@
|
||||
out=$("$KTEXT" --klammersets none -s "$TARGETS"'@@k6.ta s :one : [*s*] @@
|
||||
@@k6.tb s :two : [*s*] @@
|
||||
@k6 x @' -t ta -d 2>&1)
|
||||
if echo "$out" | grep -q "ta .*s :one" && echo "$out" | grep -q "tb .*s :two"; then
|
||||
@@ -114,7 +114,7 @@ fi
|
||||
# The error path once printed internal katom detail through msg(), which is
|
||||
# debugging scaffolding and must never reach a user-facing message.
|
||||
name=" 7. no internal debug output on the error path"
|
||||
out=$("$KTEXT" -k none -s "$TARGETS"'@@k7.k s : a declaration @@
|
||||
out=$("$KTEXT" --klammersets none -s "$TARGETS"'@@k7.k s : a declaration @@
|
||||
@@k7.ta s2 :other : [*s2*] @@
|
||||
@k7 x @' -t ta -d 2>&1)
|
||||
if echo "$out" | grep -q "klammer-definition"; then
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
# "*arg*" variables, and the closing delimiters
|
||||
# Case 14 guards the second: a comma next to an application is writer text.
|
||||
#
|
||||
# These are engine tests, so they use -k none and define their own targets
|
||||
# These are engine tests, so they use --klammersets none and define their own targets
|
||||
# inline: a target is a Machine construct (@@@target), not owned by any
|
||||
# klammer set.
|
||||
#
|
||||
@@ -54,7 +54,7 @@ trim() { awk '{ sub(/[ \t\r]+$/, "") } { line[NR]=$0 } END { f=1; while (f<=NR &
|
||||
check_eq() {
|
||||
local name="$1" target="$2" expected="$3" src="$4"
|
||||
local out status err
|
||||
out=$("$KTEXT" -k none -s "$TARGETS$src" -t "$target" -d 2>"$ERR"); status=$?
|
||||
out=$("$KTEXT" --klammersets none -s "$TARGETS$src" -t "$target" -d 2>"$ERR"); status=$?
|
||||
err=$(cat "$ERR")
|
||||
out=$(printf '%s' "$out" | trim)
|
||||
if [ $status -ne 0 ]; then
|
||||
@@ -73,18 +73,25 @@ check_eq() {
|
||||
fi
|
||||
}
|
||||
|
||||
# check_warn NAME TARGET EXPECTED SRC — exit 0, stdout==EXPECTED, AND a warning.
|
||||
# check_warn NAME TARGET EXPECTED SRC — exit 0, stdout==EXPECTED, the override
|
||||
# SILENT at the default verbosity and REPORTED at "-v 1" (2026-08-15; see
|
||||
# tst/deftype_test.sh's check_warn for why it is no longer a warning).
|
||||
check_warn() {
|
||||
local name="$1" target="$2" expected="$3" src="$4"
|
||||
local out status err
|
||||
out=$("$KTEXT" -k none -s "$TARGETS$src" -t "$target" -d 2>"$ERR"); status=$?
|
||||
err=$(cat "$ERR")
|
||||
out=$("$KTEXT" --klammersets none -s "$TARGETS$src" -t "$target" -d 2>"$ERR"); status=$?
|
||||
out=$(printf '%s' "$out" | trim)
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "${red}FAIL${reset} $name — ktext exited $status"; FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
if ! printf '%s' "$err" | grep -qiF "warning"; then
|
||||
echo "${red}FAIL${reset} $name — expected a warning, got none"; FAIL=$((FAIL+1)); return
|
||||
if [ -s "$ERR" ]; then
|
||||
echo "${red}FAIL${reset} $name — the default run was not silent: $(head -1 "$ERR")"
|
||||
FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
"$KTEXT" --klammersets none -s "$TARGETS$src" -t "$target" -d -v 1 >/dev/null 2>"$ERR"
|
||||
err=$(cat "$ERR")
|
||||
if ! printf '%s' "$err" | grep -qiF "overridden"; then
|
||||
echo "${red}FAIL${reset} $name — no override reported at -v 1"; FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
if [ "$out" = "$expected" ]; then
|
||||
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
|
||||
@@ -98,7 +105,7 @@ check_warn() {
|
||||
check_error() {
|
||||
local name="$1" target="$2" pattern="$3" src="$4"
|
||||
local out status
|
||||
out=$("$KTEXT" -k none -s "$TARGETS$src" -t "$target" -d 2>&1); status=$?
|
||||
out=$("$KTEXT" --klammersets none -s "$TARGETS$src" -t "$target" -d 2>&1); status=$?
|
||||
if [ $status -eq 0 ]; then
|
||||
echo "${red}FAIL${reset} $name — expected an error but ktext succeeded"; FAIL=$((FAIL+1)); return
|
||||
fi
|
||||
|
||||
155
tst/verbosity_test.sh
Executable file
155
tst/verbosity_test.sh
Executable file
@@ -0,0 +1,155 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# verbosity_test.sh — what "-v" says, and what it must not say.
|
||||
#
|
||||
# The output policy (CLAUDE.md, "Command output policy") fixes what each
|
||||
# verbosity level is FOR, and a policy with no test drifts back — which is how
|
||||
# the suites this session repaired got where they were. The rule:
|
||||
#
|
||||
# -v 0 silent. Nothing but the result.
|
||||
# -v 1 every decision whose outcome the user COULD NOT HAVE READ OFF THEIR
|
||||
# OWN INPUT: a klammerset symbol resolved to a file, a default applied,
|
||||
# "-o" expanded into a directory and a basename, a filename regrouped,
|
||||
# a klammer overridden by a definition in another klammer set.
|
||||
# -v 2+ the trace: what the command DID, for someone reading the code.
|
||||
#
|
||||
# The boundary is the part worth testing. Andy's first formulation was "-v 1
|
||||
# describes a change of state", and taken literally that swallows -v 2 — every
|
||||
# definition is a change of state. So the test for -v 1 is not "did something
|
||||
# happen" but "could the user have predicted the outcome from what they wrote".
|
||||
# Registering a klammer the user wrote, in the file they wrote, is NOT -v 1
|
||||
# material; resolving a symbol through a three-stage search path with shadowing
|
||||
# is.
|
||||
#
|
||||
# Engine tier: no klammerset beyond what a case loads deliberately. The SKS
|
||||
# half of this policy — font resolution and ":files" resolution — is in
|
||||
# sks/tst/verbosity_test.sh, because those are @document's decisions.
|
||||
#
|
||||
# Usage: ./verbosity_test.sh (needs KLAMMERTEXT_HOME set; commands on PATH)
|
||||
# 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'
|
||||
|
||||
OUTF=$(mktemp /tmp/verb_out.XXXXXX)
|
||||
ERRF=$(mktemp /tmp/verb_err.XXXXXX)
|
||||
TMPD=$(mktemp -d /tmp/verb_dir.XXXXXX)
|
||||
trap 'rm -f "$OUTF" "$ERRF"; rm -rf "$TMPD"' EXIT
|
||||
|
||||
plain() { sed 's/\x1b\[[0-9;]*m//g'; }
|
||||
|
||||
pass() { echo "${green}PASS${reset} $1"; PASS=$((PASS + 1)); }
|
||||
fail() { echo "${red}FAIL${reset} $1"; [ -n "$2" ] && echo " $2"; FAIL=$((FAIL + 1)); }
|
||||
|
||||
# run CMD ARGS... — fills OUTF/ERRF, sets STATUS.
|
||||
STATUS=0
|
||||
run() { "$@" >"$OUTF" 2>"$ERRF"; STATUS=$?; }
|
||||
|
||||
# reports NAME PATTERN — ERRF matches PATTERN (the log is on stderr, always).
|
||||
reports() {
|
||||
if plain < "$ERRF" | grep -Eq -- "$2"; then pass "$1"
|
||||
else fail "$1" "no [$2] in: $(plain < "$ERRF" | head -3 | tr '\n' ' ')"; fi
|
||||
}
|
||||
absent() {
|
||||
if plain < "$ERRF" | grep -Eq -- "$2"; then fail "$1" "unexpected [$2]"
|
||||
else pass "$1"; fi
|
||||
}
|
||||
|
||||
echo "${bold}Verbosity tests${reset}"
|
||||
echo "==============="
|
||||
echo
|
||||
|
||||
echo "-- -v 0 is silent --"
|
||||
run "$KTEXT" --klammersets none -s 'hello' -d
|
||||
if [ $STATUS -eq 0 ] && [ "$(cat "$OUTF")" = "hello" ] && [ ! -s "$ERRF" ]; then
|
||||
pass " 1. a successful run says nothing but its result"
|
||||
else
|
||||
fail " 1. exit $STATUS, stdout [$(cat "$OUTF")], stderr [$(head -1 "$ERRF")]"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "-- -v 1 answers \"what did the command think I asked for?\" --"
|
||||
run "$KTEXT" --klammersets none -s 'hello' -d -v 1
|
||||
reports " 2. the resolved argument list is shown" '\-s +: hello'
|
||||
reports " 3. ... including a default the user did not write" '\-v +: 1'
|
||||
# The document is on stdout and nothing shares it, whatever -v says.
|
||||
if [ "$(cat "$OUTF")" = "hello" ]; then
|
||||
pass " 4. and the result is still the only thing on stdout"
|
||||
else
|
||||
fail " 4. stdout carried [$(head -c 60 "$OUTF")]"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "-- -v 1 reports DERIVED values --"
|
||||
# The output path: the user gave a target and an input name; the directory, the
|
||||
# basename and the final filename were all constructed.
|
||||
printf 'hello\n' > "$TMPD/doc.kt"
|
||||
run "$KTEXT" "$TMPD/doc.kt" -t txt --klammersets none -v 1
|
||||
reports " 5. the output directory it constructed" 'Output directory: '
|
||||
reports " 6. the output basename it constructed" 'Output basename: doc'
|
||||
reports " 7. the output filename it constructed" 'Output filename: .*doc\.txt'
|
||||
|
||||
# A klammerset symbol resolves through a three-stage search path with
|
||||
# shadowing, so the FILE it landed on is the derived value. Both branches are
|
||||
# checked: the default (no --klammersets at all) reported nothing until
|
||||
# 2026-08-15, which was the commonest case of all.
|
||||
run "$KTEXT" -s 'x' -d -v 1
|
||||
reports " 8. the DEFAULT klammerset names the file it loaded" 'Klammerset \(default\): .*sks/sks\.k'
|
||||
mkdir -p "$TMPD/kset"
|
||||
printf '@@@klammerset kset | A set @@@\n' > "$TMPD/kset/kset.k"
|
||||
run env KLAMMERTEXT_KLAMMERSETS="$TMPD" "$KTEXT" -s 'x' -d --klammersets kset -v 1
|
||||
reports " 9. a symbol names the file it resolved to" 'Klammerset "kset": .*kset/kset\.k'
|
||||
run "$KTEXT" -s 'x' -d --klammersets none -v 1
|
||||
reports "10. and \"none\" says that none was loaded" 'Klammersets: none'
|
||||
|
||||
# An override: the definition being replaced usually lives somewhere the user
|
||||
# cannot see, which is what earns it a place here rather than a warning.
|
||||
run "$KTEXT" --klammersets none -t fx -d -v 1 \
|
||||
-s '@@@target fx | F @@@ @@w.k : W @@ @@w.fx :: A @@ @@w.fx ::: B @@ @w@'
|
||||
reports "11. a klammer overridden by a later definition" 'overridden'
|
||||
|
||||
echo
|
||||
echo "-- the boundary: -v 2 is the trace, -v 1 is not --"
|
||||
run "$KTEXT" --klammersets none -s 'hello' -d -v 1
|
||||
v1=$(wc -l < "$ERRF")
|
||||
run "$KTEXT" --klammersets none -s 'hello' -d -v 2
|
||||
v2=$(wc -l < "$ERRF")
|
||||
if [ "$v2" -gt "$v1" ]; then
|
||||
pass "12. -v 2 says more than -v 1 ($v1 lines -> $v2)"
|
||||
else
|
||||
fail "12. -v 2 added nothing ($v1 -> $v2)"
|
||||
fi
|
||||
# The trace names the code; the -v 1 report never should. A source location in
|
||||
# a level-1 line means a msg() or a stray log level, not a decision.
|
||||
run "$KTEXT" --klammersets none -s 'hello' -d -v 1
|
||||
absent "13. -v 1 does not name source files" '\[[a-z_]+\.cpp:[0-9]+\]'
|
||||
run "$KTEXT" --klammersets none -s 'hello' -d -v 2
|
||||
reports "14. ... while -v 2 does" '\[[a-z_]+\.(cpp|h):[0-9]+\]'
|
||||
|
||||
echo
|
||||
echo "-- the other two commands honour -v as well --"
|
||||
run kdesc --katoms -v 1
|
||||
if [ $STATUS -eq 0 ] && [ -s "$OUTF" ]; then
|
||||
pass "15. kdesc -v 1 still produces its result on stdout"
|
||||
else
|
||||
fail "15. exit $STATUS, stdout $(wc -c < "$OUTF") bytes"
|
||||
fi
|
||||
run kdiag -v 1 '@i-x'
|
||||
if [ $STATUS -eq 0 ] && [ -s "$OUTF" ]; then
|
||||
pass "16. kdiag -v 1 still produces its result on stdout"
|
||||
else
|
||||
fail "16. exit $STATUS, stdout $(wc -c < "$OUTF") bytes"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "==============="
|
||||
echo "Results: ${PASS} passed, ${FAIL} failed"
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
exit 0
|
||||
Reference in New Issue
Block a user