Target coverage: a klammer states the targets it serves

kdesc gains --coverage, which reports for every klammer the set of targets it
can render to, and — the point of it — which klammers' coverage cannot be
derived and must therefore be declared.  Three rules: coverage is DERIVED
where the definitions determine it (a general body of klammer calls covers
the intersection of what those klammers cover, by a greatest fixpoint after
loading), DECLARED where the engine cannot interpret what decides it (an
@eval body, whose targets are undecidable), and UNKNOWN where nothing is
written — which never means "deliberately unavailable".

Two new spellings in a definition's name.  A comma-separated target list,
"@@table.html,tex :: ...", gives one body several targets; it is surface
syntax, expanded at registration, and each member goes through the
redefinition rules on its own.  And "@@date.* :: ..." writes the general
target out, asserting that the klammer works for EVERY target including ones
not yet defined — a stronger claim than a list of the targets defined today,
and the one target declaration that could be mechanically falsified.

The Standard Klammer Set was swept accordingly: it now has no general
definitions at all, every klammer names the targets it serves, six use ".*",
and tex and pdf are at zero undecided.

kdesc's flags are reorganised on two rules: a flag reached for often gets a
single letter (-k klammers, -t targets, -c characters, -i input), a more
specialised topic a multi-letter name (--argtypes, --katoms, --rewrite,
--optionsets, --coverage, --klammerset, --font); and -v says how much to show
about PROCESSING, never what the RESULT contains — so the katom regex column
is "--katoms full" and the coverage detail "--coverage all".  NOTE: "-k" now
lists klammers (optionally filtered by a name/description search); the katom
table moved to "--katoms".

Fixes carried along: an option written with no value crashed the command with
SIGSEGV instead of reporting the mistake; two required positional arguments
never parsed; kdesc and kdiag printed an error and exited 0; and definition
diagnostics counted registrations rather than what was written, so one line
could be reported as two definitions and then printed twice.

Four new test suites: target_list, coverage, command_option, kdesc.

(from dev 46f54080bd9a)
This commit is contained in:
2026-08-12 17:20:23 +02:00
parent 61987c8b1f
commit 59c1599bc9
49 changed files with 1960 additions and 431 deletions

View File

@@ -19,4 +19,8 @@ test:
./klammerset_test.sh
./option_set_test.sh
./signature_test.sh
./target_list_test.sh
./coverage_test.sh
./command_option_test.sh
./kdesc_test.sh
./editor_test.sh

120
tst/command_option_test.sh Executable file
View File

@@ -0,0 +1,120 @@
#!/bin/bash
#
# command_option_test.sh — Command-line option parsing (mac/argv.cpp).
#
# The commands' own argument handling, which no suite covered: argv_test.cpp
# exercises Argv but asserts nothing, so two defects lived there unnoticed.
#
# * An option declared with opt() takes a value. Written LAST with nothing
# after it -- "kdesc -v", "ktext doc.kt -t" -- parse_optional() read one
# past the end of the word vector and the command died with SIGSEGV,
# naming nothing. Two bounds checks sat commented out at that spot; they
# would have returned a half-parsed option instead of reporting the
# mistake. It is now a located argument error.
# * kdesc and kdiag printed an error and then exited 0, so a script could
# not tell a failed run from a successful one. Both return 1 now, as
# ktext already did.
#
# Also here: two required positional arguments. parse_positional() consumes
# one per required name, but regex_split_prefix() requires its match at
# position 0 and returned the remainder with the separating space intact, so
# the second was always "not found". Latent, because no shipped command
# declares two -- tst/argv_test.cpp is the only program that does.
#
# Usage: ./command_option_test.sh (needs KLAMMERTEXT_HOME set; commands on PATH)
# Exit code: 0 if all tests pass, 1 otherwise.
PASS=0
FAIL=0
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
TSTDIR="$(cd "$(dirname "$0")" && pwd)"
red=$'\033[31m'
green=$'\033[32m'
bold=$'\033[1m'
reset=$'\033[0m'
# A signal death is not an error exit: 128+n, and the point of these tests is
# the difference. Report it distinctly so a crash can never read as a pass.
died_by_signal() { [ "$1" -gt 128 ]; }
# error NAME PATTERN CMD... — nonzero exit, no signal, PATTERN in the output.
error() {
local name="$1" pattern="$2"; shift 2
local out status
out=$("$@" 2>&1); status=$?
if died_by_signal $status; then
echo "${red}FAIL${reset} $name — died by signal $((status-128))"; FAIL=$((FAIL+1)); return
fi
if [ $status -eq 0 ]; then
echo "${red}FAIL${reset} $name — reported an error but exited 0"; FAIL=$((FAIL+1)); return
fi
if printf '%s' "$out" | grep -qF -- "$pattern"; then
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} $name — expected [$pattern]"
echo " got: $(printf '%s' "$out" | head -2)"; FAIL=$((FAIL+1))
fi
}
# ok NAME CMD... — exits 0 and does not die by signal.
ok() {
local name="$1"; shift
local status
"$@" >/dev/null 2>&1; status=$?
if died_by_signal $status; then
echo "${red}FAIL${reset} $name — died by signal $((status-128))"; FAIL=$((FAIL+1))
elif [ $status -ne 0 ]; then
echo "${red}FAIL${reset} $name — exit $status"; FAIL=$((FAIL+1))
else
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
fi
}
echo "${bold}Command option tests${reset}"
echo "===================="
echo
echo "-- an option written last, with no value --"
error " 1. kdesc -v" "needs a value" kdesc -v
error " 2. kdesc with a flag first" "needs a value" kdesc --katoms -v
error " 3. kdiag -v" "needs a value" kdiag -v
error " 4. ktext -t" "needs a value" ktext -s '@i-x' -t
error " 5. the message names the option" "-v <level>" kdesc -v
error " 6. ... and how to get the usage" "for the list of arguments" kdesc -v
echo
echo "-- the same options WITH a value still work --"
# NOT "kdesc -v 1": show_usage() treats exactly "<command> -v <n>" as a
# request for the usage text, which exits 1 by design. Give it another flag.
ok " 7. kdesc -c -v 1" kdesc -c -v 1
ok " 8. kdesc --katoms -v 1" kdesc --katoms -v 1
ok " 9. kdiag -v 1 '@i-x'" kdiag -v 1 '@i-x'
ok "10. ktext -t html" ktext -s '@i-x' -t html -d
echo
echo "-- an error exit is nonzero, not a printed message and exit 0 --"
error "11. kdesc reports failure" "needs a value" kdesc -v
error "12. kdiag reports failure" "needs a value" kdiag -v
echo
echo "-- two required positional arguments --"
# argv_test is the only program declaring two; it prints what it parsed.
if [ -x "$TSTDIR/argv_test" ]; then
out=$("$TSTDIR/argv_test" first second --bool 2>&1 | sed 's/\x1b\[[0-9;]*m//g')
for expect in "<input> : first" "<input2> : second"; do
if printf '%s' "$out" | grep -qF "$expect"; then
echo "${green}PASS${reset} 13/14. positional [$expect]"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} 13/14. positional [$expect] not parsed"; FAIL=$((FAIL+1))
fi
done
else
echo "SKIP 13/14. two positionals (argv_test not built; run make -C tst)"
fi
echo
echo "===================="
echo "Results: ${PASS} passed, ${FAIL} failed"
[ "$FAIL" -eq 0 ] || exit 1
exit 0

29
tst/coverage/basic.k Normal file
View File

@@ -0,0 +1,29 @@
# Fixture for coverage_test.sh. Engine tier: it declares its own targets,
# so the suite runs with no klammer set.
@@@target ta | Target A @@@
@@@target tb | Target B @@@
@@@target tc | Target C @@@
# Defined per target: coverage is what the definitions say.
@@ab.k s : defined for ta and tb @@
@@ab.ta :: A*s* @@
@@ab.tb :: B*s* @@
# A comma list is the same statement written once (step 1).
@@cd.k s : defined for ta and tc by a list @@
@@cd.ta,tc :: C*s* @@
# General body of plain text: every target.
@@plain : just text @@
# General body calling one klammer: that klammer's coverage.
@@calls_ab s : @ab *s* @ @@
# Intersection of two klammers with different coverage: only ta is in both.
@@calls_both s : @ab *s* @ @cd *s* @ @@
# ".*" is an assertion: every target, including targets not yet defined.
# Distinct from a bare definition, which asserts nothing -- that is the
# writer's macro form.
@@universal.k s : works for any target @@
@@universal.* :: U*s* @@

8
tst/coverage/clean.k Normal file
View File

@@ -0,0 +1,8 @@
# A klammerset with nothing outstanding: every klammer declared and defined.
# Used to check that a report with no problems says nothing about problems --
# no banner, no categories.
@@@target ta | Target A @@@
@@@target tb | Target B @@@
@@x.k s : declared and defined @@
@@x.ta,tb :: [*s*] @@

22
tst/coverage/cycle.k Normal file
View File

@@ -0,0 +1,22 @@
@@@target ta | Target A @@@
@@@target tb | Target B @@@
# Mutual reference between two general klammers. A recursive analysis would
# not terminate; the greatest fixpoint does.
#
# These take a parameter deliberately. A general klammer with NO parameters
# is a CONSTANT, and a constant's body is spliced into later definitions at
# definition time -- so a parameterless pair never reaches the fixpoint as a
# cycle: the expansion has already turned the second body into a
# self-reference. Parameters keep the calls in the stored body.
@@ping s : @pong *s* @ @@
@@pong s : @ping *s* @ @@
# A cycle that reaches a constrained klammer keeps the constraint.
@@only_ta.ta s : A*s* @@
@@loop_a s : @loop_b *s* @ @only_ta *s* @ @@
@@loop_b s : @loop_a *s* @ @@
# Disjoint coverage: nothing is in both, so this can never be applied.
@@only_tb.tb s : B*s* @@
@@impossible s : @only_ta *s* @ @only_tb *s* @ @@

6
tst/coverage/split.k Normal file
View File

@@ -0,0 +1,6 @@
@@@target ta | Target A @@@
@@@target tb | Target B @@@
@@x.k s : declared here, defined in two files @@
@@x.ta :: A*s* @@
@read split_more.k @

View File

@@ -0,0 +1,5 @@
# The second half of the split fixture: one klammer's targets, written in a
# different file from its declaration. Andy's policy keeps a klammer's
# targets together, so this is the exception the file column exists to make
# visible.
@@x.tb :: B*s* @@

View File

@@ -0,0 +1,10 @@
@@@target ta | Target A @@@
@@@target tb | Target B @@@
# The three bodies whose coverage the engine cannot work out.
@@evaluated : @eval 1 + 1 @ @@
@@literally : ^'\raw{markup}'^ @@
@@included : @read /dev/null @ @@
# A declaration with no definition anywhere.
@@orphan.k s : declared and never defined @@

258
tst/coverage_test.sh Executable file
View File

@@ -0,0 +1,258 @@
#!/bin/bash
#
# coverage_test.sh — Target coverage: which targets a klammer can render to.
#
# "kdesc --coverage" computes the coverage FACT and reports it. Three rules
# (mac/coverage.{h,cpp}, notes/target_coverage.md):
#
# DERIVED a general body of plain text covers every target; a general body
# of klammer calls covers the INTERSECTION of what those klammers
# cover, computed as a greatest fixpoint after loading.
# DECLARED a general body holding @eval, @read or a ^'...'^ literal span
# cannot be interpreted, so its targets must be written down.
# UNKNOWN no definition at all. Absence never means "deliberately
# unavailable" -- the SKS is incomplete on schedule, not by design.
#
# The analysis modifies nothing. These tests therefore assert only what is
# 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.
#
# Usage: ./coverage_test.sh (needs KLAMMERTEXT_HOME set; kdesc on PATH)
# Exit code: 0 if all tests pass, 1 otherwise.
PASS=0
FAIL=0
KDESC=kdesc
KTEXT=ktext
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
DIR="$(cd "$(dirname "$0")" && pwd)/coverage"
red=$'\033[31m'
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
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')
# "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
echo "${red}FAIL${reset} $f.k produced no report"; FAIL=$((FAIL+1))
fi
done
# 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
echo "${red}FAIL${reset} $name — unexpected match: $rgx"; FAIL=$((FAIL+1))
else
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
fi
}
# 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
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} $name"
echo " no line matching: $rgx"; FAIL=$((FAIL+1))
fi
}
# 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
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} $name"
echo " no line matching: $rgx"; FAIL=$((FAIL+1))
fi
}
# section_lacks NAME FIXTURE HEADER REGEX — REGEX does not appear within the
# named section. Needed wherever the same klammer legitimately appears in a
# LATER section: a whole-report "lacks" would match there and report a failure
# that is not one. A section runs from its header to the next blank line.
section_lacks() {
local name="$1" fixture="$2" header="$3" rgx="$4"
local body
body=$(printf '%s\n' "${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
fi
if printf '%s\n' "$body" | grep -Eq "$rgx"; then
echo "${red}FAIL${reset} $name — unexpected match in [$header]: $rgx"; FAIL=$((FAIL+1))
else
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
fi
}
# 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
echo "${red}FAIL${reset} $name — unexpected match: $rgx"; FAIL=$((FAIL+1))
else
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
fi
}
echo "${bold}Target coverage tests${reset}"
echo "====================="
echo
echo "-- derived coverage --"
has " 1. a general text body covers every target" basic '^All targets, derived \(1\)'
has " 2. ... and that body is @plain" basic '@plain'
has " 3. a body calling one klammer takes its targets" basic '@calls_ab +ta tb +from @ab'
has " 4. two klammers intersect" basic '@calls_both +ta +from @ab @cd'
lacks " 5. the intersection drops the target only one has" basic '@calls_both +ta tb'
echo
echo "-- what the definitions themselves say --"
has " 6. per-target definitions are not derived" basic 'Defined per target \(2\)'
has " 7. a comma list covers each target it names" basic '^ ta +6 covered'
has " 8. a target named by no definition is undecided" basic '^ tc +3 covered +3 undecided'
echo
echo "-- coverage that cannot be derived --"
has " 9. an @eval general body must be declared" undecidable '@evaluated +@eval body'
has "10. a @read general body must be declared" undecidable '@included +@read body'
has "11. a literal span must be declared" undecidable "@literally +\\^'\\.\\.\\.'\\^ literal span"
has "12. all three are counted together" undecidable '^Must be declared \(3\)'
has "13. the report says they are offered everywhere" undecidable 'currently offered by an underivable general body'
echo
echo "-- declared and never defined --"
has "14. a .k with no definitions is reported" undecidable '^Declared but never defined \(1\)'
has "15. ... and named" undecidable '^ @orphan$'
echo
echo "-- \".*\": every target, asserted --"
# The distinction the report exists to keep: a bare definition asserts
# nothing (the writer's macro form), while ".*" states that the klammer works
# for any target, including targets that do not exist yet. A list of the
# targets defined today cannot say that.
has "37. an explicit .* is its own category" basic '^All targets, declared \(1\)'
has "38. ... and covers every target" basic '^ @universal +ta tb tc'
lacks "39. it is not counted as derived" basic '^ @universal +ta tb tc$(.*)from'
section_lacks "40. nor as a bare general body" basic 'All targets, derived' '@universal'
# ".*" outranks the body scan: the point of writing it is to assert what an
# @eval body cannot be read to mean.
has "41. an @eval body under .* is not flagged" basic '^All targets, declared'
echo
echo "-- klammers with no \".k\" declaration --"
# The mirror image of the section above: that one has a declaration and no
# definitions, this one definitions and no declaration. Such a klammer works
# but has no DESCRIPTION, so kdesc can say nothing about it and "-k <text>"
# can only find it by name. Orthogonal to the coverage kinds -- a klammer can
# be defined for every target and still have none.
has "32. the section counts them" basic '^No "\.k" declaration \(3\)'
has "33. a general klammer has none" basic '^ @plain +ta tb tc$'
has "34. ... nor one defined without .k" basic '^ @calls_ab +ta tb$'
section_lacks "35. a declared klammer is not listed" basic 'No ".k" declaration' '^ @ab '
# @orphan has a .k and no definitions, so it belongs to the OTHER section.
lacks "36. the two sections do not overlap" undecidable '^No "\.k" declaration \(1\)\n @orphan'
echo
echo "-- the fixpoint --"
# A recursive analysis would not terminate on these; the report existing at
# all is most of the assertion.
has "16. mutual reference terminates" cycle '@ping +ta tb +from @pong'
has "17. ... in both directions" cycle '@pong +ta tb +from @ping'
has "18. a constraint propagates around a cycle" cycle '@loop_a +ta +from @loop_b @only_ta'
has "19. ... to the klammer that does not name it" cycle '@loop_b +ta +from @loop_a'
has "20. disjoint coverage is its own category" cycle '^Covers no target \(1\)'
has "21. ... naming the klammers to look at" cycle '^ @impossible +from @only_ta @only_tb'
section_lacks "42. and it is not listed as derived" cycle 'Derived from the klammers' '@impossible'
echo
echo "-- the source-file column (-v only) --"
# The file is the LAST column -- anchored, so a change of position is caught.
vhas "23. a klammer's file is shown, last" undecidable '@evaluated +@eval body +tst/coverage/undecidable\.k$'
vhas "24. ... in the derived section too" basic '@calls_ab +ta tb +from @ab +tst/coverage/basic\.k$'
vhas "25. ... and in the per-target listing" basic '@ab +ta tb +tst/coverage/basic\.k$'
vhas "26. a declaration with no definition" undecidable '^ @orphan +tst/coverage/undecidable\.k$'
lacks "27. no file column without -v" basic '@calls_ab +tst/coverage'
# Andy's policy keeps a klammer's targets together, so several files is the
# exception -- which is the case the column exists to make visible.
vhas "28. definitions in two files, comma-separated" split \
'@x +ta tb +tst/coverage/split\.k, tst/coverage/split_more\.k$'
# The "All targets" section lists many names on one line, so there is nothing
# for a file to attach to; it must not sprout a column.
vhas "29. the many-names row keeps its shape" basic '^ @plain$'
# A continuation line belongs to the row above it and takes no file.
lacks "30. the old continuation line is gone" cycle '^ +\^ covers no target'
echo
echo "-- the problems come last, and are grouped --"
# A terminal is read from the bottom: an 80-klammer listing scrolls a
# three-line warning off the screen, so the actionable part must be last.
has "43. a banner counts the distinct klammers" basic '^Needs attention: [0-9]+ klammers?$'
# An empty category is hidden, but the two halves hide for different reasons.
#
# A REPORTING category describes the shape of the klammer set, so an empty one
# still says something and "all" shows it as a designer's checklist.
lacks "44. an empty reporting category is hidden" cycle '^All targets, declared \(0\)'
vhas "45. ... and shown by \"all\"" cycle '^All targets, declared \(0\)'
# A PROBLEM category sits under a banner reading "Needs attention", and an
# empty one does not. Printing it there would state the opposite of the
# heading above it, so it stays hidden even under "all".
lacks "47. an empty problem category is hidden" basic '^Covers no target \(0\)'
vlacks "48. ... and stays hidden under \"all\"" basic '^Covers no target \(0\)'
# A category WITH entries is always shown, with or without "all".
has "49. a non-empty category needs no \"all\"" cycle '^Derived from the klammers the body calls \(4\)'
# The banner still counts, so "all" is not silent about the problems.
vhas "50. the banner survives under \"all\"" basic '^Needs attention: 3 klammers$'
# "No .k" is orthogonal to the others, so summing the counts would overstate.
has "46. the banner counts distinct klammers" basic '^Needs attention: 3 klammers$'
echo
echo "-- the category explanations --"
# The prose under a heading teaches the categories; a reader who knows them
# wants headings and rows. So it appears only under "all", with the empty
# categories and the file column.
lacks "51. no explanation by default" basic 'the author states that these work'
vhas "52. ... and one under \"all\"" basic 'the author states that these work'
has "53. the heading is always there" basic '^All targets, declared \(1\)'
echo
echo "-- a klammer set with nothing outstanding --"
# Nothing to attend to, nothing said: no banner and no problem categories,
# with or without "all". A banner reading "0" would contradict itself in the
# same way an empty category under it would.
lacks "54. no banner when nothing needs attention" clean '^Needs attention'
vlacks "55. ... not even under \"all\"" clean '^Needs attention'
vlacks "56. ... and no problem categories" clean '^No "\.k" declaration'
# The reporting categories still describe the set.
vhas "57. the reporting categories remain" clean '^Defined per target \(1\)'
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 @@@
@@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))
else
echo "${red}FAIL${reset} 22. an underivable klammer still renders — got [$out]"; FAIL=$((FAIL+1))
fi
echo
echo "====================="
echo "Results: ${PASS} passed, ${FAIL} failed"
[ "$FAIL" -eq 0 ] || exit 1
exit 0

158
tst/kdesc_test.sh Executable file
View File

@@ -0,0 +1,158 @@
#!/bin/bash
#
# kdesc_test.sh — the kdesc command's interface.
#
# The flag structure was reorganised 2026-08-11
# (notes/modifying_the_kdesc_arguments.md) on two rules:
#
# * 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);
# * -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.
#
# "-k" is the one rename that changes a name's MEANING rather than retiring
# it: it used to show katom types (now --katoms). A name-based guard cannot
# catch that -- the old spelling still works and does something else -- so
# these tests pin the new meaning down.
#
# Usage: ./kdesc_test.sh (needs KLAMMERTEXT_HOME set; kdesc on PATH)
# Exit code: 0 if all tests pass, 1 otherwise.
PASS=0
FAIL=0
KDESC=kdesc
K=${KLAMMERTEXT_HOME:?KLAMMERTEXT_HOME must be set}
DIR="$(cd "$(dirname "$0")" && pwd)/coverage"
red=$'\033[31m'
green=$'\033[32m'
bold=$'\033[1m'
reset=$'\033[0m'
plain() { sed 's/\x1b\[[0-9;]*m//g'; }
# 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=$?
if [ $status -gt 128 ]; then
echo "${red}FAIL${reset} $name — died by signal $((status-128))"; FAIL=$((FAIL+1)); return
fi
if printf '%s' "$out" | grep -Eq -- "$pattern"; then
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} $name — no line matching [$pattern]"
echo " got: $(printf '%s' "$out" | head -2)"; FAIL=$((FAIL+1))
fi
}
# 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
echo "${red}FAIL${reset} $name — unexpected match [$pattern]"; FAIL=$((FAIL+1))
else
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
fi
}
echo "${bold}kdesc interface tests${reset}"
echo "====================="
echo
echo "-- the single-letter flags --"
shows " 1. -k lists klammers" '@table' "$KDESC" -k
shows " 2. -t lists targets" 'html' "$KDESC" -t
shows " 3. -c lists character codes" '.' "$KDESC" -c
shows " 4. -i reads the named input" '@x' "$KDESC" -i "$DIR/split.k" -k
echo
echo "-- the multi-letter topics --"
shows " 5. --katoms lists katom types" 'katom' "$KDESC" --katoms
shows " 6. --argtypes lists argtypes" 'fraction' "$KDESC" --argtypes
shows " 7. --rewrite lists rewrites" '.' "$KDESC" --rewrite
shows " 8. --optionsets lists sets" 'caption_args' "$KDESC" --optionsets
echo
echo "-- result detail is a word, not a verbosity level --"
shows " 9. --katoms full adds the regex column" 'Regex' "$KDESC" --katoms full
absent "10. --katoms alone omits it" 'Regex' "$KDESC" --katoms
shows "11. --coverage all adds the file column" 'sks/table/table\.k' "$KDESC" --coverage all
absent "12. --coverage alone omits it" 'sks/table/table\.k' "$KDESC" --coverage
# "all" is one word for both kinds of missing information: if you want some of
# it you probably want all of it. It shows an empty REPORTING category -- a
# designer's checklist of the shapes a klammer set can have -- but not an
# empty PROBLEM one, which would state the opposite of the "Needs attention"
# banner above it.
# Against a FIXTURE, not the SKS: which categories are empty there changes as
# the klammer set is worked on, and a test pinned to that state fails for a
# 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
absent "12b. ... hidden without it" \
'All targets, declared \(0\)' "$KDESC" -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
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
echo
echo "-- the klammer search --"
shows "16. a name matches" '@table' "$KDESC" -k table
absent "17. ... and others are excluded" '@document' "$KDESC" -k table
shows "18. matching is case-insensitive" '@table' "$KDESC" -k TABLE
shows "19. a description word matches" '@' "$KDESC" -k verbatim
# The words after -k are one phrase, and whitespace is collapsed on both
# sides -- a description written across several lines in a .k file must still
# match a phrase typed on one.
shows "20. several words are one phrase" '@' "$KDESC" -k displayed verbatim
shows "21. ... with runs of space collapsed" '@' "$KDESC" -k displayed verbatim
shows "22. no match says so" 'No klammer names or descriptions contained "zzqq"\.' "$KDESC" -k zzqq
absent "23. ... and lists nothing" '^ @' "$KDESC" -k zzqq
echo
echo "-- the subcommand words --"
shows "24. --coverage help explains" 'Coverage commands' "$KDESC" --coverage help
shows "25. --katoms help explains" 'Katom commands' "$KDESC" --katoms help
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 "-- 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)
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
echo "${green}PASS${reset} 30. -k is listed before --katoms"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} 30. usage order: -k at line $k_line, --katoms at $katoms_line"
FAIL=$((FAIL+1))
fi
echo
echo "-- a no-result search is not an error --"
timeout 60 "$KDESC" -k zzqq >/dev/null 2>&1
if [ $? -eq 0 ]; then
echo "${green}PASS${reset} 31. finding nothing exits 0"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} 31. finding nothing exits nonzero"; FAIL=$((FAIL+1))
fi
echo
echo "====================="
echo "Results: ${PASS} passed, ${FAIL} failed"
[ "$FAIL" -eq 0 ] || exit 1
exit 0

View File

@@ -135,7 +135,7 @@ accepted " 8. a .k declaration with :: instances" \
rejected " 9. a .k declaration plus a parameterized definition" \
'@@k9.k s : a declaration @@
@@k9.ta s2 :other : [*s2*] @@
@k9 x @' 'both a declaration'
@k9 x @' 'both a ".k" declaration'
echo
echo "============================="

215
tst/target_list_test.sh Executable file
View File

@@ -0,0 +1,215 @@
#!/bin/bash
#
# target_list_test.sh — A klammer definition may name several targets.
#
# @@table.html,tex :: <body> @@
#
# One body, several targets. The list is SURFACE SYNTAX: the registry makes
# one definition per target named, so nothing downstream of registration knows
# a list was written. That is what these tests pin down — in particular that
# each member goes through the redefinition transition table on its own, so a
# list overlapping an existing definition is decided per target rather than
# all-or-nothing.
#
# Why the feature exists: a klammer whose body is an `@eval` cannot have its
# coverage derived (deciding which targets a Python function answers for is
# undecidable), so its targets must be DECLARED. Writing one definition per
# target would then duplicate the body. See notes/target_coverage.md.
#
# Two patterns are involved and must not be confused (mac/ktype.h):
# definition_begin_name — runs through commas; used ONLY for "@@<name>"
# definition_name — no comma; klammer applications, option names,
# "*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
# inline: a target is a Machine construct (@@@target), not owned by any
# klammer set.
#
# Usage: ./target_list_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}
ERR=/tmp/target_list_test_err.$$
red=$'\033[31m'
green=$'\033[32m'
bold=$'\033[1m'
reset=$'\033[0m'
# ta, tb, tc are independent; td INCLUDES tc, so tc "provides" td and a
# definition for tc is copied to td (mac/klammer_registry.cpp).
TARGETS='@@@target ta | Target A @@@
@@@target tb | Target B @@@
@@@target tc | Target C @@@
@@@target td | Target D :includes tc @@@
'
trim() { awk '{ sub(/[ \t\r]+$/, "") } { line[NR]=$0 } END { f=1; while (f<=NR && line[f]=="") f++; l=NR; while (l>=1 && line[l]=="") l--; for (i=f;i<=l;i++) print line[i] }'; }
# check_eq NAME TARGET EXPECTED SRC — exit 0, stdout==EXPECTED, no warning.
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=$?
err=$(cat "$ERR")
out=$(printf '%s' "$out" | trim)
if [ $status -ne 0 ]; then
echo "${red}FAIL${reset} $name — ktext exited $status"
echo " stderr: $(echo "$err" | head -2)"; FAIL=$((FAIL+1)); return
fi
if printf '%s' "$err" | grep -qiF "warning"; then
echo "${red}FAIL${reset} $name — unexpected warning"
echo " stderr: $(echo "$err" | head -2)"; FAIL=$((FAIL+1)); return
fi
if [ "$out" = "$expected" ]; then
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} $name"
echo " expected: [$expected]"; echo " got: [$out]"; FAIL=$((FAIL+1))
fi
}
# check_warn NAME TARGET EXPECTED SRC — exit 0, stdout==EXPECTED, AND 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=$(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
fi
if [ "$out" = "$expected" ]; then
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} $name"
echo " expected: [$expected]"; echo " got: [$out]"; FAIL=$((FAIL+1))
fi
}
# check_error NAME TARGET PATTERN SRC — nonzero exit and PATTERN in the message.
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=$?
if [ $status -eq 0 ]; then
echo "${red}FAIL${reset} $name — expected an error but ktext succeeded"; FAIL=$((FAIL+1)); return
fi
if echo "$out" | grep -qF "$pattern"; then
echo "${green}PASS${reset} $name"; PASS=$((PASS+1))
else
echo "${red}FAIL${reset} $name — expected error to contain [$pattern]"
echo " output: $(echo "$out" | head -3)"; FAIL=$((FAIL+1))
fi
}
echo "${bold}Klammer definition target-list tests${reset}"
echo "===================================="
echo
# --- One body, several targets ---
echo "-- the list defines each target named --"
check_eq " 1. first member gets the body" ta "x B y" \
'@@f.k : a test @@ @@f.ta,tb :: B @@ x @f@ y'
check_eq " 2. second member gets the body" tb "x B y" \
'@@f.k : a test @@ @@f.ta,tb :: B @@ x @f@ y'
check_error " 3. a target NOT in the list is undefined" tc "not defined for target" \
'@@f.k : a test @@ @@f.ta,tb :: B @@ x @f@ y'
check_eq " 4. three members, last one" tc "x B y" \
'@@f.k : a test @@ @@f.ta,tb,tc :: B @@ x @f@ y'
check_eq " 5. parameters are inherited from .k for every member" tb "x [q] y" \
'@@f.k s : a test @@ @@f.ta,tb :: [*s*] @@ x @f q @ y'
check_eq " 6. a list member propagates to a target that includes it" td "x B y" \
'@@f.k : a test @@ @@f.ta,tc :: B @@ x @f@ y'
# --- Malformed lists ---
echo
echo "-- malformed lists --"
check_error " 7. an unknown target in the list" ta "is not defined" \
'@@f.ta,nosuch : B @@'
check_error " 8. a target named twice" ta "more than once" \
'@@f.ta,ta : B @@'
check_error " 9. \".k\" in a list" ta "in a list of targets" \
'@@f.k,ta : a test @@'
check_error "10. \".o\" in a list" ta "in a list of targets" \
'@@f.o,ta : an option set @@'
# "*" is the general target written out: an assertion that the klammer works
# for EVERY target, including ones not yet defined. In a list it is either
# redundant or a misunderstanding.
check_error "10a. \"*\" in a list" ta 'names "*" in a list of targets' \
'@@f.*,ta : b @@'
check_eq "10b. \"*\" alone defines every target" tb "x body" \
'@@f.* : body @@ x @f@'
check_eq "10c. ... including one it does not name" ta "x body" \
'@@f.* : body @@ x @f@'
check_error "11. a trailing comma" ta "is not correctly defined" \
'@@f.ta, : B @@'
check_error "12. an empty member" ta "is not correctly defined" \
'@@f.ta,,tb : B @@'
# --- The transition table applies per member, not to the list ---
echo
echo "-- redefinition, decided per member --"
check_error "13. create + list create collides on the shared member" ta "already defined" \
'@@f.ta : one @@ @@f.ta,tb : two @@ x @f@'
# The list's default is silently ignored for ta (which already has a create)
# and creates tb. This is the case a whole-list decision would get wrong.
check_eq "14. existing create + list default: existing kept" ta "x one" \
'@@f.ta : one @@ @@f.ta,tb :::: two @@ x @f@'
check_eq "15. existing create + list default: other member defined" tb "x two" \
'@@f.ta : one @@ @@f.ta,tb :::: two @@ x @f@'
check_eq "16. list default + create for one member: replaced" ta "x real" \
'@@f.ta,tb :::: def @@ @@f.ta : real @@ x @f@'
check_eq "17. list default + create for one member: other keeps default" tb "x def" \
'@@f.ta,tb :::: def @@ @@f.ta : real @@ x @f@'
check_warn "18. list override warns and replaces (first member)" ta "x over" \
'@@f.k : a test @@ @@f.ta :: one @@ @@f.tb :: two @@ @@f.ta,tb ::: over @@ x @f@'
check_warn "19. list override warns and replaces (second member)" tb "x over" \
'@@f.k : a test @@ @@f.ta :: one @@ @@f.tb :: two @@ @@f.ta,tb ::: over @@ x @f@'
# --- The comma is a target separator ONLY after "@@" ---
echo
echo "-- a comma elsewhere is writer text --"
check_eq "20. a comma in a body is text" ta "x a,b" \
'@@f.ta : a,b @@ x @f@'
check_eq "21. a comma after an argument substitution" ta "x q,tail" \
'@@f.ta s : *s*,tail @@ x @f q @'
check_eq "22. a comma after an application" ta "x q, y" \
'@@f.ta s : *s* @@ x @f q @, y'
check_eq "23. a general definition is unaffected" ta "x B" \
'@@f : B @@ x @f@'
# --- Diagnostics count what was WRITTEN, not how often it registered ---
echo
echo "-- one definition, several registrations --"
# tc provides td, so "@@f.tc : ..." registers twice from one line. Before
# this was grouped, the message said "2 definitions" and then printed the one
# line the author wrote twice, sending them to look for a second.
check_error "24. the count is of written definitions" ta \
"and a definition that declares its own parameters" \
'@@f.k s : d @@ @@f.tc : [*s*] @@'
check_error "25. ... and the location names its targets" ta "(targets tc, td)" \
'@@f.k s : d @@ @@f.tc : [*s*] @@'
# Two definitions in two places are still two.
check_error "26. distinct places still count separately" ta \
"2 definitions that declare their own parameters" \
'@@f.k s : d @@ @@f.ta : a*s* @@ @@f.tb : b*s* @@'
# The remedy, named in the message rather than implied.
check_error "27. the message says what to write instead" ta \
'Write "::" instead of ":"' \
'@@f.k s : d @@ @@f.tc : [*s*] @@'
rm -f "$ERR"
echo
echo "===================================="
echo "Results: ${PASS} passed, ${FAIL} failed"
[ "$FAIL" -eq 0 ] || exit 1
exit 0