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.
297 lines
12 KiB
Bash
Executable File
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 ]
|