Files
klammertext/tst/editor_test.sh
T
ack fe475e774a Klammertext runs natively on Windows; a Windows source-install guide; the bundled sh for @eval :shell
Assembled from klammertext-dev f344a37d0f6e. The commands, the library,
the Standard Klammer Set, the reflection klammerset and the Python
binding build and run natively on Windows (MSYS2 UCRT64 gcc, python.org
Python), and both test tiers pass there. doc/install/windows_source_install.md
is the fifth install guide: a source install parallel to the Linux and
macOS ones, verified by following it on a pristine Windows 11.

Behaviour on every system: paths Klammertext prints use forward slashes;
output is LF; the command line is UTF-8 on Windows; @eval :shell is a
POSIX shell command everywhere -- on Windows it runs the sh Klammertext
ships, BusyBox for Windows (win/busybox/, GPLv2, with its licence and the
source pointer). KLAMMERTEXT_FONTS and KLAMMERTEXT_KLAMMERSETS are lists in
the system's PATH convention (';' on Windows). On Linux and macOS nothing
observable changes.
2026-09-25 00:32:55 +02:00

297 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) -------------------------------
# The interpreter's name: python3 on Linux and macOS; on Windows python.org
# installs "python" only, and env/runtime.env exports PYTHON there.
PYTHON=${PYTHON:-python3}
if ! command -v "$PYTHON" >/dev/null 2>&1; then
echo "editor_test.sh: $PYTHON not found" >&2
exit 1
fi
if ! PYTHONDONTWRITEBYTECODE=1 "$PYTHON" "$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 "$PYTHON" -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 "$PYTHON" "$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 "$PYTHON" -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 ]