2026-07-18 18:48:23 +02:00
|
|
|
#!/bin/bash
|
|
|
|
|
#
|
|
|
|
|
# cond_test.sh — Regression tests for @cond argument delimitation.
|
|
|
|
|
#
|
|
|
|
|
# These tests pin down the fix for the bug witnessed by tst/cond_test.kt:
|
|
|
|
|
# a defined klammer that contains its own bar separators (e.g. @frac a | b @)
|
|
|
|
|
# nested inside a @cond branch caused @cond to miscount bars and reject the
|
|
|
|
|
# input with "There should only be one or two bar characters".
|
|
|
|
|
#
|
|
|
|
|
# Root cause: @cond delimited its arguments by counting EVERY bar in its flat
|
|
|
|
|
# katom range, conflating the inner klammer's bars (which belong to the inner
|
|
|
|
|
# klammer's arity) with @cond's own separators. The fix counts only the bars
|
|
|
|
|
# at nesting depth 0 within the @cond span (cond_separator_bars() in
|
|
|
|
|
# mac/machine.cpp), so argument boundaries follow the span tree.
|
|
|
|
|
#
|
|
|
|
|
# See doc/cond_evaluation_order.md for the full description and the
|
|
|
|
|
# theoretical basis (operadic arity, the precedence-order proposition, and
|
|
|
|
|
# @cond as a non-strict special form).
|
|
|
|
|
#
|
Typed arguments, calculated tables, spans, closed-world fonts, top-level fnt/ and env/
Sync with klammertext-dev through b90b0e09:
- Argument types end to end: :python_cast values are applied (Python
@eval receives real bools/numbers/lists), argument values are
validated against their argtype patterns with the argtype's
description as the error message, argtypes can declare :default
(overridable per declaration), and parameterized type families are
supported: rest(N) casts a rest argument to an N-dimensional list
(bar-count = dimension).
- Unified indexed_range syntax (selector with parenthesized subsets,
composable mnemonic names) for table lines and spans.
- Table klammer: caption fonts fixed in both targets, :column_width /
:leading / :colsep wired, :colspan and :rowspan render (HTML
attributes; \multicolumn / \multirow), calculated cell values (:calc)
with prefix operators, display-precision semantics, :calc_format and
:decimal period|comma.
- Fonts: closed-world resolution on the Klammertext font store
(infrastructure in mac/font_store; no Google Fonts links or fetch).
Default fonts live in the top-level fnt/; additional fonts install
into KLAMMERTEXT_FONTS directories via kdesc --font (list, samples,
preview, install — classification by font metadata). CSS font family
names are quoted (digit-initial families were silently lost).
- Environment files moved from mac/env/ to the top-level env/; shell
profiles source env/runtime.env. Dead per-host variants removed.
- Container: fnt/ ships in the image; curl removed (no network use).
2026-07-22 18:17:43 +02:00
|
|
|
# Usage: ./cond_test.sh (LSan suppressions come from env/runtime.env.*)
|
2026-07-18 18:48:23 +02:00
|
|
|
# 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'
|
|
|
|
|
|
|
|
|
|
# strip leading/trailing blank lines and surrounding whitespace
|
2026-07-19 17:44:03 +02:00
|
|
|
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] }'; }
|
2026-07-18 18:48:23 +02:00
|
|
|
|
|
|
|
|
# check_eq TEST_NAME EXPECTED KTEXT_ARGS...
|
|
|
|
|
# Runs ktext, expects exit status 0, and compares trimmed stdout to EXPECTED.
|
|
|
|
|
check_eq() {
|
|
|
|
|
local test_name="$1"
|
|
|
|
|
local expected="$2"
|
|
|
|
|
shift 2
|
|
|
|
|
|
|
|
|
|
local output status
|
|
|
|
|
output=$("$KTEXT" "$@" 2>/tmp/cond_test_err.$$)
|
|
|
|
|
status=$?
|
|
|
|
|
output=$(printf '%s' "$output" | trim)
|
|
|
|
|
|
|
|
|
|
if [ $status -ne 0 ]; then
|
|
|
|
|
echo "${red}FAIL${reset} $test_name — ktext exited $status"
|
|
|
|
|
echo " stderr: $(head -3 /tmp/cond_test_err.$$)"
|
|
|
|
|
FAIL=$((FAIL + 1))
|
|
|
|
|
return
|
|
|
|
|
fi
|
|
|
|
|
if [ "$output" = "$expected" ]; then
|
|
|
|
|
echo "${green}PASS${reset} $test_name"
|
|
|
|
|
PASS=$((PASS + 1))
|
|
|
|
|
else
|
|
|
|
|
echo "${red}FAIL${reset} $test_name"
|
|
|
|
|
echo " expected: [$expected]"
|
|
|
|
|
echo " got: [$output]"
|
|
|
|
|
FAIL=$((FAIL + 1))
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# check_contains TEST_NAME SUBSTRING KTEXT_ARGS...
|
|
|
|
|
# Runs ktext, expects exit status 0, and checks that stdout contains SUBSTRING.
|
|
|
|
|
check_contains() {
|
|
|
|
|
local test_name="$1"
|
|
|
|
|
local needle="$2"
|
|
|
|
|
shift 2
|
|
|
|
|
|
|
|
|
|
local output status
|
|
|
|
|
output=$("$KTEXT" "$@" 2>&1)
|
|
|
|
|
status=$?
|
|
|
|
|
|
|
|
|
|
if [ $status -ne 0 ]; then
|
|
|
|
|
echo "${red}FAIL${reset} $test_name — ktext exited $status"
|
|
|
|
|
echo " output: $(echo "$output" | head -3)"
|
|
|
|
|
FAIL=$((FAIL + 1))
|
|
|
|
|
return
|
|
|
|
|
fi
|
|
|
|
|
if echo "$output" | grep -qF "$needle"; then
|
|
|
|
|
echo "${green}PASS${reset} $test_name"
|
|
|
|
|
PASS=$((PASS + 1))
|
|
|
|
|
else
|
|
|
|
|
echo "${red}FAIL${reset} $test_name — expected to contain [$needle]"
|
|
|
|
|
echo " output: $(echo "$output" | head -3)"
|
|
|
|
|
FAIL=$((FAIL + 1))
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# check_error TEST_NAME PATTERN KTEXT_ARGS...
|
|
|
|
|
# Runs ktext, expects a NONZERO exit status and PATTERN in the message.
|
|
|
|
|
check_error() {
|
|
|
|
|
local test_name="$1"
|
|
|
|
|
local pattern="$2"
|
|
|
|
|
shift 2
|
|
|
|
|
|
|
|
|
|
local output status
|
|
|
|
|
output=$("$KTEXT" "$@" 2>&1)
|
|
|
|
|
status=$?
|
|
|
|
|
|
|
|
|
|
if [ $status -eq 0 ]; then
|
|
|
|
|
echo "${red}FAIL${reset} $test_name — expected an error but ktext succeeded"
|
|
|
|
|
FAIL=$((FAIL + 1))
|
|
|
|
|
return
|
|
|
|
|
fi
|
|
|
|
|
if echo "$output" | grep -qF "$pattern"; then
|
|
|
|
|
echo "${green}PASS${reset} $test_name"
|
|
|
|
|
PASS=$((PASS + 1))
|
|
|
|
|
else
|
|
|
|
|
echo "${red}FAIL${reset} $test_name — expected error to contain [$pattern]"
|
|
|
|
|
echo " output: $(echo "$output" | head -3)"
|
|
|
|
|
FAIL=$((FAIL + 1))
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FRAC='@@frac a | b : *a*/*b* @@'
|
|
|
|
|
|
An output policy for the three commands, and @cond as a true special form
A snapshot of the development tree. The substantial changes since the last one:
COMMAND OUTPUT POLICY. The three commands display text in exactly three cases,
and each owns a stream: LOGGING under "-v" greater than 0 and an ERROR before
termination go to STDERR; OUTPUT THE USER ASKED FOR goes to STDOUT. For ktext
that output is a document, so "ktext doc.kt -d | ..." is now safe -- logging
used to share the stream and land inside the document. A bare command prints
its usage and succeeds rather than failing. Colour is emitted only to a
terminal, per stream, and NO_COLOR is honoured.
"-v 1" reports every decision whose outcome you could not have read off your own
input: the klammerset that was loaded and from which file, a font's directory, a
":files" name's file, how "-o" was expanded. Higher levels are the trace.
The commands no longer warn and continue: an anomaly is an error, described with
its location. Two exceptions remain, each for a stated reason -- a condition
that is expected and temporary by design, and a judgment that is a heuristic
rather than exact.
@cond IS NOW A TRUE SPECIAL FORM, resolved at APPLICATION time rather than when
the file is read. Two consequences for a writer:
* a state variable reaches the predicate. "@@@state Flag :value true @@@
@cond *Flag* | T | F @" renders "T"; it used to see the literal "*Flag*" and
silently take the false branch. The document now behaves like a klammer
body, whose arguments are bound before its conditionals are decided.
* nothing in a discarded branch happens -- it is not read, not evaluated, not
expanded. An @eval in the branch not taken used to run anyway.
Its predicate relation is total and strict: true, True, 1; false, False, 0, and
empty; anything else is an error at the @cond rather than silently false.
@eval REACHING OUTSIDE. ":shell" and ":haskell" now keep the command's standard
error out of the document (it appears under "-v 1") and treat a nonzero exit as
an error naming what the command reported. A command that exits nonzero on
purpose -- "grep" finding no match -- says so with "|| true".
KLAMMER SETS. Several combine: "--klammersets a b c" loads all three in the
order given, sharing one namespace, with the definition modes deciding
collisions. "none" means none and may not be combined with other symbols. A
klammerset with symbol X is declared in a file X/X.k, which is what lets two
sets require the same third set without loading it twice.
TESTS. Four new suites: the kdiag command's interface, the @eval primitive's
contract with the outside world, and verbosity at both tiers. Three suites
that could not run on macOS at all now do.
Assembled from dev commit 6c8ee6c22fca.
2026-08-16 01:37:59 +02:00
|
|
|
# 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
|
|
|
|
|
|
2026-07-18 18:48:23 +02:00
|
|
|
echo "${bold}@cond argument delimitation tests${reset}"
|
|
|
|
|
echo "================================="
|
|
|
|
|
echo
|
|
|
|
|
|
|
|
|
|
# --- The reported regression (defined klammer with bars inside a branch) ---
|
|
|
|
|
|
|
|
|
|
check_eq \
|
|
|
|
|
" 1. inner klammer with bars, true branch" \
|
|
|
|
|
"1/2" \
|
|
|
|
|
-s "$FRAC @cond true | @frac 1 | 2 @ | @frac 2 | 1 @ @" -d
|
|
|
|
|
|
|
|
|
|
check_eq \
|
|
|
|
|
" 2. inner klammer with bars, false branch" \
|
|
|
|
|
"2/1" \
|
|
|
|
|
-s "$FRAC @cond false | @frac 1 | 2 @ | @frac 2 | 1 @ @" -d
|
|
|
|
|
|
|
|
|
|
# --- The exact witness file from the bug report ---
|
|
|
|
|
|
|
|
|
|
check_contains \
|
|
|
|
|
" 3. tst/cond_test.kt @cond_klammer_test true" \
|
|
|
|
|
"1/2" \
|
|
|
|
|
"$K/tst/cond_test.kt" -d -s '@cond_klammer_test true @'
|
|
|
|
|
|
|
|
|
|
check_contains \
|
|
|
|
|
" 4. tst/cond_test.kt @cond_klammer_test false" \
|
|
|
|
|
"2/1" \
|
|
|
|
|
"$K/tst/cond_test.kt" -d -s '@cond_klammer_test false @'
|
|
|
|
|
|
|
|
|
|
# --- Plain @cond unaffected by the change ---
|
|
|
|
|
|
|
|
|
|
check_eq " 5. plain two-bar, true" "yes" -s '@cond true | yes | no @' -d
|
|
|
|
|
check_eq " 6. plain two-bar, false" "no" -s '@cond false | yes | no @' -d
|
|
|
|
|
check_eq " 7. one-bar, true" "shown" -s '@cond true | shown @' -d
|
|
|
|
|
check_eq " 8. one-bar, false (empty)" "" -s '@cond false | shown @' -d
|
|
|
|
|
|
|
|
|
|
# --- Primitives and nesting inside @cond ---
|
|
|
|
|
|
|
|
|
|
check_eq " 9. @eval in a branch" "42" -s '@cond true | @eval 6*7 @ | no @' -d
|
|
|
|
|
check_eq "10. @eval as the predicate" "yes" -s '@cond @eval 1==1 @ | yes | no @' -d
|
|
|
|
|
check_eq "11. nested @cond in a branch" "B" -s '@cond true | @cond false | A | B @ | C @' -d
|
|
|
|
|
|
|
|
|
|
# --- A klammer with its own bars (double-bar / cells) inside a branch ---
|
|
|
|
|
|
|
|
|
|
check_eq "12. klammer with internal bar in a branch" \
|
|
|
|
|
"x+y" \
|
|
|
|
|
-s '@@two a | b : *a*+*b* @@ @cond true | @two x | y @ | z @' -d
|
|
|
|
|
|
|
|
|
|
# --- Genuine arity errors must still be rejected ---
|
|
|
|
|
|
|
|
|
|
check_error "13. three top-level bars is still an error" \
|
|
|
|
|
"one or two bar characters" \
|
|
|
|
|
-s '@cond true | a | b | c @' -d
|
|
|
|
|
|
|
|
|
|
check_error "14. zero bars is still an error" \
|
|
|
|
|
"one or two bar characters" \
|
|
|
|
|
-s '@cond true @' -d
|
|
|
|
|
|
An output policy for the three commands, and @cond as a true special form
A snapshot of the development tree. The substantial changes since the last one:
COMMAND OUTPUT POLICY. The three commands display text in exactly three cases,
and each owns a stream: LOGGING under "-v" greater than 0 and an ERROR before
termination go to STDERR; OUTPUT THE USER ASKED FOR goes to STDOUT. For ktext
that output is a document, so "ktext doc.kt -d | ..." is now safe -- logging
used to share the stream and land inside the document. A bare command prints
its usage and succeeds rather than failing. Colour is emitted only to a
terminal, per stream, and NO_COLOR is honoured.
"-v 1" reports every decision whose outcome you could not have read off your own
input: the klammerset that was loaded and from which file, a font's directory, a
":files" name's file, how "-o" was expanded. Higher levels are the trace.
The commands no longer warn and continue: an anomaly is an error, described with
its location. Two exceptions remain, each for a stated reason -- a condition
that is expected and temporary by design, and a judgment that is a heuristic
rather than exact.
@cond IS NOW A TRUE SPECIAL FORM, resolved at APPLICATION time rather than when
the file is read. Two consequences for a writer:
* a state variable reaches the predicate. "@@@state Flag :value true @@@
@cond *Flag* | T | F @" renders "T"; it used to see the literal "*Flag*" and
silently take the false branch. The document now behaves like a klammer
body, whose arguments are bound before its conditionals are decided.
* nothing in a discarded branch happens -- it is not read, not evaluated, not
expanded. An @eval in the branch not taken used to run anyway.
Its predicate relation is total and strict: true, True, 1; false, False, 0, and
empty; anything else is an error at the @cond rather than silently false.
@eval REACHING OUTSIDE. ":shell" and ":haskell" now keep the command's standard
error out of the document (it appears under "-v 1") and treat a nonzero exit as
an error naming what the command reported. A command that exits nonzero on
purpose -- "grep" finding no match -- says so with "|| true".
KLAMMER SETS. Several combine: "--klammersets a b c" loads all three in the
order given, sharing one namespace, with the definition modes deciding
collisions. "none" means none and may not be combined with other symbols. A
klammerset with symbol X is declared in a file X/X.k, which is what lets two
sets require the same third set without loading it twice.
TESTS. Four new suites: the kdiag command's interface, the @eval primitive's
contract with the outside world, and verbosity at both tiers. Three suites
that could not run on macOS at all now do.
Assembled from dev commit 6c8ee6c22fca.
2026-08-16 01:37:59 +02:00
|
|
|
# --- 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
|
|
|
|
|
|
2026-07-18 18:48:23 +02:00
|
|
|
rm -f /tmp/cond_test_err.$$
|
|
|
|
|
|
|
|
|
|
echo
|
|
|
|
|
echo "================================="
|
|
|
|
|
echo "Results: ${green}$PASS passed${reset}, ${red}$FAIL failed${reset}"
|
|
|
|
|
[ $FAIL -eq 0 ]
|