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.
320 lines
12 KiB
Bash
Executable File
320 lines
12 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# option_set_test.sh — Regression tests for option sets (the ".o" target).
|
|
#
|
|
# An option set is a named group of OPTIONAL parameters, declared once and
|
|
# used by several klammers, so that a writer learns one vocabulary instead of
|
|
# a spelling per klammer:
|
|
#
|
|
# @@caption_args.o :caption :number.bool true : A caption @@
|
|
# @@table.k rows.rest(2) @caption_args@ : A table of rows of cells @@
|
|
#
|
|
# The rules this suite holds to:
|
|
#
|
|
# * "o" is a pseudo-target beside "k". A ".o" declaration defines no
|
|
# klammer and produces no output for any target.
|
|
# * A set declares optional parameters only. A positional is not
|
|
# writer-facing, so there is nothing for a set to standardize.
|
|
# * Names and types come from the set; a DEFAULT may be overridden where
|
|
# the set is used, because what varies between klammers is only what
|
|
# silence means for that one klammer.
|
|
# * A set may be used only in the parameter list of a ".k" declaration --
|
|
# the one place a klammer's interface is declared once for all of its
|
|
# targets. Not in a per-target definition, and not in another set.
|
|
# * A klammer application in a parameter list is an error. Splicing a
|
|
# constant klammer there used to be the way to share parameters, and in
|
|
# a ".k" declaration it silently destroyed the whole parameter list.
|
|
#
|
|
# Engine tier: no SKS. Every fixture defines its own target inline.
|
|
#
|
|
# Usage: ./option_set_test.sh
|
|
# Exit code: 0 if all tests pass, 1 otherwise.
|
|
|
|
PASS=0
|
|
FAIL=0
|
|
KTEXT=ktext
|
|
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
|
|
|
|
red=$'\033[31m'
|
|
green=$'\033[32m'
|
|
bold=$'\033[1m'
|
|
reset=$'\033[0m'
|
|
|
|
# The fixture prelude: a target, and two sets to use in declarations.
|
|
PRELUDE='@@@target fix | a fixture target @@@
|
|
@@cap.o :caption :number.bool true :side bottom : A caption for an element @@
|
|
@@pos.o :hpos left : Where an element sits @@'
|
|
|
|
# check_eq NAME EXPECTED SOURCE [TARGET] — render SOURCE (for the fix target
|
|
# unless TARGET says otherwise) and compare the trimmed output with EXPECTED.
|
|
check_eq() {
|
|
local name="$1" expected="$2" source="$3" target="${4:-fix}"
|
|
local output status
|
|
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:]]*$//')
|
|
if [ $status -ne 0 ]; then
|
|
echo "${red}FAIL${reset} $name — ktext exited $status"
|
|
echo " output: $(printf '%s' "$output" | head -3)"
|
|
FAIL=$((FAIL + 1))
|
|
return
|
|
fi
|
|
if [ "$output" = "$expected" ]; then
|
|
echo "${green}PASS${reset} $name"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo "${red}FAIL${reset} $name"
|
|
echo " expected: [$expected]"
|
|
echo " got: [$output]"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
}
|
|
|
|
# check_fails NAME SUBSTRING SOURCE — SOURCE must be rejected, with a
|
|
# message containing SUBSTRING.
|
|
check_fails() {
|
|
local name="$1" needle="$2" source="$3"
|
|
local output status
|
|
output=$("$KTEXT" --klammersets none -s "$PRELUDE
|
|
$source" -t fix -d 2>&1)
|
|
status=$?
|
|
if [ $status -eq 0 ]; then
|
|
echo "${red}FAIL${reset} $name — expected an error, ktext exited 0"
|
|
FAIL=$((FAIL + 1))
|
|
return
|
|
fi
|
|
if printf '%s' "$output" | tr '\n' ' ' | grep -qF "$needle"; then
|
|
echo "${green}PASS${reset} $name"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo "${red}FAIL${reset} $name"
|
|
echo " expected error containing: [$needle]"
|
|
echo " got: $(printf '%s' "$output" | tr '\n' ' ' | head -c 300)"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
}
|
|
|
|
# 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 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"
|
|
FAIL=$((FAIL + 1))
|
|
return
|
|
fi
|
|
if printf '%s' "$output" | tr '\n' ' ' | grep -qF "$needle"; then
|
|
echo "${green}PASS${reset} $name"
|
|
PASS=$((PASS + 1))
|
|
else
|
|
echo "${red}FAIL${reset} $name"
|
|
echo " expected a warning containing: [$needle]"
|
|
echo " got: $(printf '%s' "$output" | tr '\n' ' ' | head -c 300)"
|
|
FAIL=$((FAIL + 1))
|
|
fi
|
|
}
|
|
|
|
echo "${bold}Option set (.o) tests${reset}"
|
|
echo "====================="
|
|
echo
|
|
|
|
# --- The members become the klammer's parameters --------------------------
|
|
|
|
# A declaration using a set accepts the set's options, and the writer's
|
|
# arguments reach the body under the names the set declares.
|
|
check_eq " 1. a set's parameters are the klammer's" \
|
|
"[A|top]" \
|
|
'@@e.k x @cap@ : an element @@
|
|
@@e.fix :: [*caption*|*side*] @@
|
|
@e X :caption A :side top @'
|
|
|
|
# Absent argument, declared default: the set supplies it.
|
|
check_eq " 2. a member's default applies when the argument is absent" \
|
|
"[|true|bottom]" \
|
|
'@@e.k x @cap@ : an element @@
|
|
@@e.fix :: [*caption*|*number*|*side*] @@
|
|
@e X @'
|
|
|
|
# The use site may override a default -- and only the default.
|
|
check_eq " 3. a use-site override changes the default" \
|
|
"[|true|top]" \
|
|
'@@e.k x @cap :side top @ : an element @@
|
|
@@e.fix :: [*caption*|*number*|*side*] @@
|
|
@e X @'
|
|
|
|
check_eq " 4. a boolean default can be overridden to false" \
|
|
"[false]" \
|
|
'@@e.k x @cap :number false @ : an element @@
|
|
@@e.fix :: [*number*] @@
|
|
@e X @'
|
|
|
|
# An option written alone at the use site takes the argument type's :alone
|
|
# value, exactly as it does where an argument is written.
|
|
check_eq " 5. an override written alone takes the argtype's alone value" \
|
|
"[true]" \
|
|
'@@n.o :number.bool false : A number @@
|
|
@@e.k x @n :number @ : an element @@
|
|
@@e.fix :: [*number*] @@
|
|
@e X @'
|
|
|
|
# Three levels: argument type, option set, use site -- and the writer's
|
|
# argument still wins over all of them.
|
|
check_eq " 6. a written argument wins over the overridden default" \
|
|
"[maybe]" \
|
|
'@@e.k x @cap :side top @ : an element @@
|
|
@@e.fix :: [*side*] @@
|
|
@e X :side maybe @'
|
|
|
|
check_eq " 7. two sets in one declaration" \
|
|
"[bottom|left]" \
|
|
'@@e.k x @cap@ @pos@ : an element @@
|
|
@@e.fix :: [*side*|*hpos*] @@
|
|
@e X @'
|
|
|
|
check_eq " 8. a set's parameters mix with the klammer's own" \
|
|
"[own|bottom]" \
|
|
'@@e.k x :mine own @cap@ : an element @@
|
|
@@e.fix :: [*mine*|*side*] @@
|
|
@e X @'
|
|
|
|
# Every target definition is an instance, so all of them get the expanded
|
|
# list: the set is resolved once, in the declaration.
|
|
check_eq " 9. a second target's instance inherits the same parameters" \
|
|
"<bottom>" \
|
|
'@@@target fix2 | another fixture target @@@
|
|
@@e.k x @cap@ : an element @@
|
|
@@e.fix :: [*side*] @@
|
|
@@e.fix2 :: <*side*> @@
|
|
@e X @' \
|
|
fix2
|
|
|
|
# --- The declaration is not a klammer -------------------------------------
|
|
|
|
check_fails "10. a set defines no klammer" \
|
|
'The klammer "cap" is not defined' \
|
|
'@cap@'
|
|
|
|
# --- Where a set may be used ----------------------------------------------
|
|
|
|
check_fails "11. not in a per-target definition" \
|
|
'may be used only in the parameter list of a ".k" declaration' \
|
|
'@@e.fix x @cap@ : [*caption*] @@'
|
|
|
|
check_fails "12. not in a general definition" \
|
|
'may be used only in the parameter list of a ".k" declaration' \
|
|
'@@e x @cap@ : [*caption*] @@'
|
|
|
|
check_fails "13. not in another set" \
|
|
'a set does not include another set' \
|
|
'@@both.o :extra @cap@ : two vocabularies @@'
|
|
|
|
# --- A klammer application in a parameter list ----------------------------
|
|
|
|
check_fails "14. a klammer application in a parameter list is rejected" \
|
|
'A klammer application in a parameter list is not allowed' \
|
|
'@@c : :spliced @@
|
|
@@e.k x @c@ : an element @@'
|
|
|
|
check_fails "15. ... and the message names the declared sets" \
|
|
'Declared option sets: cap, pos' \
|
|
'@@e.k x @nosuch@ : an element @@'
|
|
|
|
# --- Use-site overrides ---------------------------------------------------
|
|
|
|
check_fails "16. an override must name a member of the set" \
|
|
'The option set "cap" has no parameter ":nope"' \
|
|
'@@e.k x @cap :nope 1 @ : an element @@'
|
|
|
|
check_fails "17. an override value is validated at definition time" \
|
|
'does not match the "bool" argument type' \
|
|
'@@e.k x @cap :number perhaps @ : an element @@'
|
|
|
|
check_fails "18. an override may not restate the type" \
|
|
'only a default may be given where it is used' \
|
|
'@@e.k x @cap :number.bool false @ : an element @@'
|
|
|
|
check_fails "19. an override may not give a positional value" \
|
|
'gives a value that is not an option' \
|
|
'@@e.k x @cap here @ : an element @@'
|
|
|
|
# --- What a set may declare -----------------------------------------------
|
|
|
|
check_fails "20. a positional parameter is rejected" \
|
|
'An option set declares only optional parameters' \
|
|
'@@bad.o p :q : oops @@'
|
|
|
|
check_fails "21. a rest parameter is rejected" \
|
|
'An option set declares only optional parameters' \
|
|
'@@bad.o r.rest :q : oops @@'
|
|
|
|
check_fails "22. a set with no parameters is rejected" \
|
|
'declares no parameters' \
|
|
'@@bad.o : nothing at all @@'
|
|
|
|
check_fails '23. "::" has no meaning for a set' \
|
|
'An option set IS a declaration' \
|
|
'@@cap.o :: nope @@'
|
|
|
|
# --- Collisions -----------------------------------------------------------
|
|
|
|
check_fails "24. two sets declaring the same name name both sets" \
|
|
'from the option set "cap"' \
|
|
'@@other.o :side right : another side @@
|
|
@@e.k x @cap@ @other@ : an element @@'
|
|
|
|
check_fails "25. a set colliding with a declared parameter" \
|
|
'declared in the parameter list' \
|
|
'@@e.k x :side own @cap@ : an element @@'
|
|
|
|
# --- Redefinition ---------------------------------------------------------
|
|
|
|
check_fails "26. declaring a set twice is an error" \
|
|
'Option set "cap.o" already defined' \
|
|
'@@cap.o :caption : a second caption @@'
|
|
|
|
# A set is its own declaration, so an override restates what it declares --
|
|
# there is no ".k" for it to inherit a parameter list from.
|
|
check_warns '27. ":::" overrides a set, with a warning' \
|
|
'overridden' \
|
|
'@@cap.o :caption :number.bool true :side top ::: a replaced caption @@'
|
|
|
|
check_warns "28. ... and the overriding declaration is what a klammer gets" \
|
|
"[top]" \
|
|
'@@cap.o :caption :number.bool true :side top ::: a replaced caption @@
|
|
@@e.k x @cap@ : an element @@
|
|
@@e.fix :: [*side*] @@
|
|
@e X @'
|
|
|
|
# A "::::" default is silently superseded by a later create, and the create
|
|
# is what the using declaration gets.
|
|
check_eq "29. a default set is superseded by a later declaration" \
|
|
"[right]" \
|
|
'@@d.o :where left :::: a default @@
|
|
@@d.o :where right : the real one @@
|
|
@@e.k x @d@ : an element @@
|
|
@@e.fix :: [*where*] @@
|
|
@e X @'
|
|
|
|
echo
|
|
echo "====================="
|
|
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
|
|
[ $FAIL -eq 0 ]
|