#!/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" -k 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" -k 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). check_warns() { local name="$1" needle="$2" source="$3" local output status output=$("$KTEXT" -k none -s "$PRELUDE $source" -t fix -d 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" \ "" \ '@@@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 ]