Files
klammertext/tst/editor_test.sh
T
ack e4f1307aa9 Characters that killed a render, what may sit next to a klammer, and ref/
A character written in the ^...^ mnemonic form -- ^oe^, ^zz^, ^hello^ --
used to kill ktext outright: no message, no output, and an exit status
that looked like a crash because it was one.  Only the forms that happen
to spell hexadecimal digits escaped, and they were right by luck rather
than by reading.  Any caret form that names no character is now a located
error that quotes what was written.

A klammer may now sit inside square brackets and braces as it already
could inside parentheses, and may be followed by the punctuation that
ends a sentence: [@i word@], {@b word@}, @i word@'s.  The rule is a fixed
list of characters, because none of them can occur inside a klammer name
and so none of them is ambiguous next to a delimiter.  A letter still
cannot: "x@" is the named closing delimiter, so a letter before "@" has
two readings and no rule can separate them.

The abbreviations an author writes -- the hyphen form @i-word, a klammer
glued to its neighbouring punctuation -- are now recognised as author
conveniences only.  They are not applied to @@ definitions or @@@ system
commands, which are written by someone who needs no such shorthand and
whose forms are already whole.

Also fixed, each of them silent: an exported shell function in the
environment broke every @eval in the document; a line break written with
@nl was destroyed when it passed through a klammer that generates a
table, printing as \textbackslash{}newline in the finished PDF; and
"kdesc --coverage" shortened its file column by comparing the spelling of
a path rather than the path, so a klammer set reached by a symbolic link
showed a full path where its neighbours showed a short one.

New in the distribution: ref/, a small klammerset that describes the
Klammermachine's own tables in Klammertext.  It currently provides the
character tables -- the diacritics and the extended Latin letters -- as
klammers that render to any target, so the documentation of what can be
written cannot drift from what the machine accepts.  It is early, and
will grow.

The Klammertext taxonomy now has a section on targets, describes the
@@@klammerset system command alongside the other three, and states the
precedence of "^" -- what a caret means before a letter, a digit, a
punctuation character, and a quote, in one ordered list.

(from dev 6594c7ff8c1f)
2026-09-07 22:37:05 +02:00

294 lines
12 KiB
Bash
Executable File

#!/bin/bash
#
# editor_test.sh — Regression tests for the Klammertext editor support
# (doc/edit/: the shared Python core, the language server, and the Emacs,
# Sublime Text, Vim, and VS Code integrations built on them).
#
# Why this lives in tst/: the editor code implements the LANGUAGE's structural
# layer — @-run length, bar-run dimension, ^-escapes, # removal, literal
# spans, nesting depth — independently of both the SKS and the Klammermachine.
# tst/ is the SKS-independent tier, and these suites check that the
# implementations of Klammertext's structure agree with the canonical
# formatting conventions AND with each other. The klammer names that appear
# in the fixtures (@ol, @table, @document, @code, @c) are seeded configuration of
# the editor tools, not SKS dependencies; nothing here runs ktext or loads a
# klammer set.
#
# What is checked, for every fixture pair <name>.kt / <name>_expected.kt in
# tst/editor/:
# * the shared core's output equals the expected file — through the Python
# API (what Sublime uses) AND through the core's CLI (what Vim uses)
# * idempotence: the tool applied to the expected file leaves it unchanged
# * the three Sublime adapter plugins import outside Sublime and resolve
# to the shared core
# * the language server passes its protocol test (tst/editor/ls_test.py:
# handshake, diagnostics, formatting, matching, alignTable — the VS Code
# extension's whole surface)
# * when Emacs is installed: the Emacs units' output equals the expected
# file, and equals the shared core's output byte for byte (the sync
# check across the independent elisp implementation) — skipped otherwise
# * when a Vim with +eval is installed: the Vim plugin's commands produce
# the same bytes (exercising the CLI shell-out glue) — skipped otherwise
#
# The editor code lives in doc/edit/ (same layout in the development tree and
# the distribution).
#
# Usage: ./editor_test.sh Requires python3; Emacs and Vim optional.
# Exit code: 0 if all tests pass, 1 otherwise.
PASS=0
FAIL=0
red=$'\033[31m'
green=$'\033[32m'
bold=$'\033[1m'
reset=$'\033[0m'
HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/.." && pwd)"
EDIT_DIR="$ROOT/doc/edit"
if [ ! -d "$EDIT_DIR/emacs" ]; then
echo "editor_test.sh: cannot locate the editor support (doc/edit)" >&2
exit 1
fi
EMACS_DIR="$EDIT_DIR/emacs"
SUBLIME_DIR="$EDIT_DIR/sublime"
SHARED_DIR="$EDIT_DIR/shared"
VIM_DIR="$EDIT_DIR/vim"
FIX="$HERE/editor"
OUT="$(mktemp -d)"
trap 'rm -rf "$OUT"' EXIT
INDENT_FIXTURES="indent_list indent_document indent_table indent_untouched
indent_defs indent_escapes indent_named_close indent_literal_c"
ALIGN_FIXTURES="align_mixed align_empty_cells align_boundary align_colspan
align_escapes align_too_wide"
# Build the (MODE INFILE OUTFILE) triples: each fixture is run from its input
# and from its expected file (the idempotence check).
py_args=()
el_args=()
add_fixture() { # add_fixture MODE NAME
local mode="$1" name="$2"
py_args+=("$mode" "$FIX/$name.kt" "$OUT/$name.py.out")
py_args+=("$mode" "$FIX/${name}_expected.kt" "$OUT/$name.py.idem")
py_args+=("$mode-cli" "$FIX/$name.kt" "$OUT/$name.cli.out")
el_args+=("$mode" "$FIX/$name.kt" "$OUT/$name.el.out")
el_args+=("$mode" "$FIX/${name}_expected.kt" "$OUT/$name.el.idem")
}
for f in $INDENT_FIXTURES; do add_fixture indent "$f"; done
for f in $ALIGN_FIXTURES; do add_fixture align "$f"; done
check() { # check NAME FILE_A FILE_B
if diff -q "$2" "$3" >/dev/null 2>&1; then
echo "${green}PASS${reset} $1"
PASS=$((PASS + 1))
else
echo "${red}FAIL${reset} $1"
diff "$2" "$3" | head -10
FAIL=$((FAIL + 1))
fi
}
# --- the shared core: API and CLI (required) -------------------------------
if ! command -v python3 >/dev/null 2>&1; then
echo "editor_test.sh: python3 not found" >&2
exit 1
fi
if ! PYTHONDONTWRITEBYTECODE=1 python3 "$FIX/editor_driver.py" "$SHARED_DIR" "${py_args[@]}"; then
echo "editor_test.sh: the Python driver failed" >&2
exit 1
fi
# --- the Sublime adapters resolve to the shared core -----------------------
if PYTHONDONTWRITEBYTECODE=1 python3 -c "
import sys
sys.path.insert(0, '$SUBLIME_DIR')
import Klammertext, Klammertext_indent, Klammertext_align
assert Klammertext.KE.__file__.startswith('$SHARED_DIR')
" 2>"$OUT/sublime.log"; then
echo "${green}PASS${reset} sublime adapters import the shared core"
PASS=$((PASS + 1))
else
echo "${red}FAIL${reset} sublime adapters import the shared core"
cat "$OUT/sublime.log"
FAIL=$((FAIL + 1))
fi
# --- the language server protocol test -------------------------------------
if PYTHONDONTWRITEBYTECODE=1 python3 "$FIX/ls_test.py" "$SHARED_DIR" "$FIX" >"$OUT/ls.log" 2>&1; then
echo "${green}PASS${reset} language server protocol ($(grep -c '^PASS' "$OUT/ls.log") checks)"
PASS=$((PASS + 1))
else
echo "${red}FAIL${reset} language server protocol:"
cat "$OUT/ls.log"
FAIL=$((FAIL + 1))
fi
# run_bounded SECONDS CMD... -- run CMD with a wall-clock limit, returning
# 137 if it had to be killed. An external editor that is INSTALLED BUT
# NON-FUNCTIONAL is not hypothetical: Olion's Homebrew emacs blocks forever
# on "emacs --batch" with no CPU used, and because this suite had no bound it
# sat for 2h43m producing nothing (2026-09-05). A hang is worse than a
# failure -- it is indistinguishable from slowness, so nobody investigates.
#
# No "timeout(1)": macOS does not have it (nor gtimeout without coreutils),
# and this suite ships. Background the command, race a killer against it.
# "set -m" makes the job its own PROCESS GROUP so the whole group can be
# killed. Killing only the pid leaves the children: a hung editor's orphaned
# child still holds the inherited stdout, and anything reading this suite
# through a pipe then blocks anyway -- the timeout fires and the hang
# continues, which is the worst of both. Measured while writing this.
run_bounded() {
local secs="$1"; shift
set -m
"$@" & local pid=$!
set +m
( sleep "$secs"; kill -9 -"$pid" 2>/dev/null || kill -9 "$pid" 2>/dev/null ) \
>/dev/null 2>&1 & local killer=$!
wait "$pid" 2>/dev/null; local st=$?
kill "$killer" 2>/dev/null; wait "$killer" 2>/dev/null
return $st
}
EDITOR_TIMEOUT=${EDITOR_TIMEOUT:-120}
# --- the Emacs units (optional) --------------------------------------------
HAVE_EMACS=0
if command -v emacs >/dev/null 2>&1; then
run_bounded "$EDITOR_TIMEOUT" \
emacs --batch -L "$EMACS_DIR" \
-l klammertext-mode -l klammertext-indent -l klammertext-align \
-l "$FIX/editor_driver.el" "${el_args[@]}" 2>"$OUT/emacs.log"
emacs_status=$?
if [ "$emacs_status" = 0 ]; then
HAVE_EMACS=1
elif [ "$emacs_status" = 137 ]; then
# Distinguished from a failure on purpose: "it hung" and "it said no"
# need different investigations, and only one of them is about
# Klammertext.
echo "${red}FAIL${reset} the Emacs driver did not finish within" \
"${EDITOR_TIMEOUT}s and was killed (emacs is installed but not" \
"working on this machine)" >&2
FAIL=$((FAIL + 1))
else
echo "${red}FAIL${reset} the Emacs driver failed:" >&2
cat "$OUT/emacs.log" >&2
FAIL=$((FAIL + 1))
fi
else
echo "(Emacs not installed — the Emacs half is skipped; the shared core still runs)"
fi
# --- the Vim plugin (optional) ---------------------------------------------
HAVE_VIM=0
VIM_BIN="$(command -v vim || true)"
if [ -n "$VIM_BIN" ] && "$VIM_BIN" --version 2>/dev/null | grep -q '+eval'; then
HAVE_VIM=1
run_vim() { # run_vim MODE INFILE OUTFILE
local cmd
if [ "$1" = indent ]; then
cmd='KlammertextReindent'
else
cmd='call search("|") | KlammertextAlign'
fi
"$VIM_BIN" -N -n -u NONE -i NONE -es --not-a-term \
--cmd "set rtp^=$VIM_DIR" \
-c 'filetype plugin on' \
-c "edit! $2" -c 'set ft=klammertext' \
-c "$cmd" -c "saveas! $3" -c 'qa!' </dev/null >/dev/null 2>&1
}
for f in $INDENT_FIXTURES; do
run_vim indent "$FIX/$f.kt" "$OUT/$f.vim.out"
run_vim indent "$FIX/${f}_expected.kt" "$OUT/$f.vim.idem"
done
for f in $ALIGN_FIXTURES; do
run_vim align "$FIX/$f.kt" "$OUT/$f.vim.out"
run_vim align "$FIX/${f}_expected.kt" "$OUT/$f.vim.idem"
done
# The comprehensive plugin checks: ftdetect (Kotlin override), syntax
# token classes, jump-to-match (incl. multibyte columns), the location-
# list check, and — with +python3 — indentexpr (gg=G) and the live
# match highlighter. Sections skip inside the driver per Vim feature.
if KT_FIX="$FIX" KT_OUT="$OUT/vim_feature.txt" \
"$VIM_BIN" -N -n -u NONE -i NONE -es --not-a-term \
--cmd "set rtp^=$VIM_DIR" \
-c "source $FIX/vim_feature_test.vim" </dev/null >/dev/null 2>&1; then
echo "${green}PASS${reset} vim plugin features ($(grep -c '^PASS' "$OUT/vim_feature.txt") checks$(
grep -q '^SKIP' "$OUT/vim_feature.txt" && printf '; %s' "$(grep -c '^SKIP' "$OUT/vim_feature.txt") section(s) skipped"))"
PASS=$((PASS + 1))
else
echo "${red}FAIL${reset} vim plugin features:"
grep -v '^PASS' "$OUT/vim_feature.txt" 2>/dev/null || echo " (the Vim driver itself failed)"
FAIL=$((FAIL + 1))
fi
else
echo "(no Vim with +eval installed — the Vim half is skipped; the shared core still runs)"
fi
# --- the VS Code extension (optional: needs a Node runtime) ----------------
# node itself, or VS Code's Electron binary run as Node.
run_node() {
if command -v node >/dev/null 2>&1; then
node "$@"
elif [ -x /usr/share/code/code ]; then
ELECTRON_RUN_AS_NODE=1 /usr/share/code/code "$@"
else
return 127
fi
}
if command -v node >/dev/null 2>&1 || [ -x /usr/share/code/code ]; then
if run_node "$FIX/vscode_ext_test.js" "$EDIT_DIR/vscode" "$FIX" >"$OUT/vscode.log" 2>&1; then
echo "${green}PASS${reset} vscode extension ($(grep -c '^PASS' "$OUT/vscode.log") checks)"
PASS=$((PASS + 1))
else
echo "${red}FAIL${reset} vscode extension:"
cat "$OUT/vscode.log"
FAIL=$((FAIL + 1))
fi
else
echo "(no Node runtime — the VS Code extension test is skipped; the language server is still tested)"
fi
# --- the VS Code .vsix package builds with the full payload ----------------
# (the no-marketplace install path; needs only bash + python3)
mkdir -p "$OUT/vsix"
cp -R "$EDIT_DIR/vscode" "$EDIT_DIR/shared" "$OUT/vsix/"
if ( cd "$OUT/vsix/vscode" && ./make_vsix.sh >/dev/null 2>&1 ) && \
PYTHONDONTWRITEBYTECODE=1 python3 -c "
import sys, zipfile
names = set(zipfile.ZipFile('$OUT/vsix/vscode/klammertext.vsix').namelist())
need = {'extension.vsixmanifest', '[Content_Types].xml',
'extension/package.json', 'extension/extension.js',
'extension/language-configuration.json', 'extension/README.md',
'extension/klammertext_edit.py', 'extension/klammertext_ls.py',
'extension/syntaxes/klammertext.tmLanguage.json'}
sys.exit(1 if need - names else 0)
"; then
echo "${green}PASS${reset} vscode .vsix package builds (make_vsix.sh)"
PASS=$((PASS + 1))
else
echo "${red}FAIL${reset} vscode .vsix package builds (make_vsix.sh)"
FAIL=$((FAIL + 1))
fi
# --- compare ---------------------------------------------------------------
for f in $INDENT_FIXTURES $ALIGN_FIXTURES; do
check "$f (python)" "$OUT/$f.py.out" "$FIX/${f}_expected.kt"
check "$f (python idempotent)" "$OUT/$f.py.idem" "$FIX/${f}_expected.kt"
check "$f (cli)" "$OUT/$f.cli.out" "$FIX/${f}_expected.kt"
if [ "$HAVE_EMACS" = 1 ]; then
check "$f (emacs)" "$OUT/$f.el.out" "$FIX/${f}_expected.kt"
check "$f (emacs idempotent)" "$OUT/$f.el.idem" "$FIX/${f}_expected.kt"
check "$f (emacs == python)" "$OUT/$f.el.out" "$OUT/$f.py.out"
fi
if [ "$HAVE_VIM" = 1 ]; then
check "$f (vim)" "$OUT/$f.vim.out" "$FIX/${f}_expected.kt"
check "$f (vim idempotent)" "$OUT/$f.vim.idem" "$FIX/${f}_expected.kt"
fi
done
echo
echo "${bold}editor_test: $PASS passed, $FAIL failed${reset}"
[ "$FAIL" -eq 0 ]