#!/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) 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 .kt / _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" 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 # --- the Emacs units (optional) -------------------------------------------- HAVE_EMACS=0 if command -v emacs >/dev/null 2>&1; then if 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"; then HAVE_EMACS=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 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 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 ]