|
| 1 | +#!/usr/bin/env bash |
| 2 | +# SPDX-License-Identifier: PMPL-1.0-or-later |
| 3 | +# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk> |
| 4 | +# |
| 5 | +# vscode-k9 — End-to-End Tests |
| 6 | +# |
| 7 | +# Validates the K9 VS Code extension manifest, grammar files, snippets, |
| 8 | +# language configuration, and example K9 contract file structure. |
| 9 | +# No VS Code runtime required — these are static validation tests. |
| 10 | +# |
| 11 | +# Usage: |
| 12 | +# bash tests/e2e.sh |
| 13 | +# just e2e |
| 14 | + |
| 15 | +set -euo pipefail |
| 16 | + |
| 17 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 18 | +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" |
| 19 | + |
| 20 | +PASS=0 |
| 21 | +FAIL=0 |
| 22 | +SKIP=0 |
| 23 | + |
| 24 | +# ─── Colour helpers ────────────────────────────────────────────────── |
| 25 | +green() { printf '\033[32m%s\033[0m\n' "$*"; } |
| 26 | +red() { printf '\033[31m%s\033[0m\n' "$*"; } |
| 27 | +yellow(){ printf '\033[33m%s\033[0m\n' "$*"; } |
| 28 | +bold() { printf '\033[1m%s\033[0m\n' "$*"; } |
| 29 | + |
| 30 | +# ─── Assertion helpers ─────────────────────────────────────────────── |
| 31 | + |
| 32 | +check() { |
| 33 | + local name="$1" expected="$2" actual="$3" |
| 34 | + if echo "$actual" | grep -q "$expected"; then |
| 35 | + green " PASS: $name" |
| 36 | + PASS=$((PASS + 1)) |
| 37 | + else |
| 38 | + red " FAIL: $name (expected '$expected', got '${actual:0:120}')" |
| 39 | + FAIL=$((FAIL + 1)) |
| 40 | + fi |
| 41 | +} |
| 42 | + |
| 43 | +# check_json_valid <label> <file> |
| 44 | +check_json_valid() { |
| 45 | + local name="$1" file="$2" |
| 46 | + if python3 -c "import json; json.load(open('$file'))" 2>/dev/null; then |
| 47 | + green " PASS: $name is valid JSON" |
| 48 | + PASS=$((PASS + 1)) |
| 49 | + else |
| 50 | + red " FAIL: $name is NOT valid JSON ($file)" |
| 51 | + FAIL=$((FAIL + 1)) |
| 52 | + fi |
| 53 | +} |
| 54 | + |
| 55 | +check_exists() { |
| 56 | + local name="$1" path="$2" |
| 57 | + if [ -e "$path" ]; then |
| 58 | + green " PASS: $name" |
| 59 | + PASS=$((PASS + 1)) |
| 60 | + else |
| 61 | + red " FAIL: $name (path not found: $path)" |
| 62 | + FAIL=$((FAIL + 1)) |
| 63 | + fi |
| 64 | +} |
| 65 | + |
| 66 | +skip_test() { |
| 67 | + yellow " SKIP: $1 ($2)" |
| 68 | + SKIP=$((SKIP + 1)) |
| 69 | +} |
| 70 | + |
| 71 | +echo "═══════════════════════════════════════════════════════════════" |
| 72 | +echo " vscode-k9 — End-to-End Tests" |
| 73 | +echo "═══════════════════════════════════════════════════════════════" |
| 74 | +echo "" |
| 75 | + |
| 76 | +# ─── Section 1: Required extension files ───────────────────────────── |
| 77 | +bold "Section 1: Required extension files" |
| 78 | + |
| 79 | +check_exists "package.json exists" "$PROJECT_DIR/package.json" |
| 80 | +check_exists "language-configuration.json exists" "$PROJECT_DIR/language-configuration.json" |
| 81 | +check_exists "language-configuration-nickel.json exists" "$PROJECT_DIR/language-configuration-nickel.json" |
| 82 | +check_exists "syntaxes/k9.tmLanguage.json exists" "$PROJECT_DIR/syntaxes/k9.tmLanguage.json" |
| 83 | +check_exists "syntaxes/k9-nickel.tmLanguage.json exists" "$PROJECT_DIR/syntaxes/k9-nickel.tmLanguage.json" |
| 84 | +check_exists "snippets/k9.json exists" "$PROJECT_DIR/snippets/k9.json" |
| 85 | +check_exists "snippets/k9-nickel.json exists" "$PROJECT_DIR/snippets/k9-nickel.json" |
| 86 | +check_exists "icons/ directory exists" "$PROJECT_DIR/icons" |
| 87 | + |
| 88 | +echo "" |
| 89 | + |
| 90 | +# ─── Section 2: package.json manifest validation ───────────────────── |
| 91 | +bold "Section 2: Extension manifest (package.json)" |
| 92 | + |
| 93 | +check_json_valid "package.json" "$PROJECT_DIR/package.json" |
| 94 | + |
| 95 | +PKG=$(cat "$PROJECT_DIR/package.json") |
| 96 | +check "package.json has name field" '"name"' "$PKG" |
| 97 | +check "package.json has displayName" '"displayName"' "$PKG" |
| 98 | +check "package.json has description" '"description"' "$PKG" |
| 99 | +check "package.json has publisher" '"publisher".*hyperpolymath' "$PKG" |
| 100 | +check "package.json has vscode engine" '"vscode"' "$PKG" |
| 101 | +check "package.json contributes languages" '"languages"' "$PKG" |
| 102 | +check "package.json contributes grammars" '"grammars"' "$PKG" |
| 103 | +check "package.json contributes snippets" '"snippets"' "$PKG" |
| 104 | +check "package.json references k9 language" '"k9"' "$PKG" |
| 105 | +check "package.json has license field" '"license"' "$PKG" |
| 106 | +check "package.json has repository" '"repository"' "$PKG" |
| 107 | + |
| 108 | +echo "" |
| 109 | + |
| 110 | +# ─── Section 3: Grammar files (TextMate) ───────────────────────────── |
| 111 | +bold "Section 3: TextMate grammar files" |
| 112 | + |
| 113 | +check_json_valid "k9.tmLanguage.json" "$PROJECT_DIR/syntaxes/k9.tmLanguage.json" |
| 114 | +check_json_valid "k9-nickel.tmLanguage.json" "$PROJECT_DIR/syntaxes/k9-nickel.tmLanguage.json" |
| 115 | + |
| 116 | +K9_GRAMMAR=$(cat "$PROJECT_DIR/syntaxes/k9.tmLanguage.json") |
| 117 | +check "k9 grammar has scopeName" "scopeName" "$K9_GRAMMAR" |
| 118 | +check "k9 grammar has patterns" "patterns" "$K9_GRAMMAR" |
| 119 | +check "k9 grammar has source.k9" "source.k9" "$K9_GRAMMAR" |
| 120 | +check "k9 grammar has fileTypes" "fileTypes" "$K9_GRAMMAR" |
| 121 | + |
| 122 | +K9NCL_GRAMMAR=$(cat "$PROJECT_DIR/syntaxes/k9-nickel.tmLanguage.json") |
| 123 | +check "k9-nickel grammar has scopeName" "scopeName" "$K9NCL_GRAMMAR" |
| 124 | +check "k9-nickel grammar has source.k9" "source.k9" "$K9NCL_GRAMMAR" |
| 125 | + |
| 126 | +echo "" |
| 127 | + |
| 128 | +# ─── Section 4: Snippets file validation ───────────────────────────── |
| 129 | +bold "Section 4: Snippets files" |
| 130 | + |
| 131 | +check_json_valid "k9.json snippets" "$PROJECT_DIR/snippets/k9.json" |
| 132 | +check_json_valid "k9-nickel.json snippets" "$PROJECT_DIR/snippets/k9-nickel.json" |
| 133 | + |
| 134 | +K9_SNIPPETS=$(cat "$PROJECT_DIR/snippets/k9.json") |
| 135 | +# Snippets should be a non-empty JSON object |
| 136 | +if python3 -c " |
| 137 | +import json, sys |
| 138 | +d = json.load(open('$PROJECT_DIR/snippets/k9.json')) |
| 139 | +if not isinstance(d, dict) or len(d) == 0: |
| 140 | + sys.exit(1) |
| 141 | +# Each snippet must have 'body' and 'prefix' |
| 142 | +for name, snip in d.items(): |
| 143 | + if 'body' not in snip or 'prefix' not in snip: |
| 144 | + print(f'Snippet {name} missing body or prefix') |
| 145 | + sys.exit(1) |
| 146 | +" 2>/dev/null; then |
| 147 | + green " PASS: k9 snippets have valid body/prefix structure" |
| 148 | + PASS=$((PASS + 1)) |
| 149 | +else |
| 150 | + red " FAIL: k9 snippets structure invalid (missing body or prefix in entries)" |
| 151 | + FAIL=$((FAIL + 1)) |
| 152 | +fi |
| 153 | + |
| 154 | +echo "" |
| 155 | + |
| 156 | +# ─── Section 5: Language configuration ─────────────────────────────── |
| 157 | +bold "Section 5: Language configuration files" |
| 158 | + |
| 159 | +check_json_valid "language-configuration.json" "$PROJECT_DIR/language-configuration.json" |
| 160 | +check_json_valid "language-configuration-nickel.json" "$PROJECT_DIR/language-configuration-nickel.json" |
| 161 | + |
| 162 | +LC=$(cat "$PROJECT_DIR/language-configuration.json") |
| 163 | +check "language config has comments" "comments" "$LC" |
| 164 | +check "language config has brackets" "brackets" "$LC" |
| 165 | + |
| 166 | +echo "" |
| 167 | + |
| 168 | +# ─── Section 6: Grammar–manifest consistency ───────────────────────── |
| 169 | +bold "Section 6: Grammar–manifest consistency" |
| 170 | + |
| 171 | +# Grammar paths in package.json should match actual files |
| 172 | +GRAMMAR_PATHS=$(python3 -c " |
| 173 | +import json |
| 174 | +p = json.load(open('$PROJECT_DIR/package.json')) |
| 175 | +for g in p.get('contributes', {}).get('grammars', []): |
| 176 | + print(g['path']) |
| 177 | +" 2>/dev/null || echo "") |
| 178 | + |
| 179 | +if [ -n "$GRAMMAR_PATHS" ]; then |
| 180 | + while IFS= read -r rel_path; do |
| 181 | + abs_path="$PROJECT_DIR/${rel_path#./}" |
| 182 | + if [ -f "$abs_path" ]; then |
| 183 | + green " PASS: grammar path exists: $rel_path" |
| 184 | + PASS=$((PASS + 1)) |
| 185 | + else |
| 186 | + red " FAIL: grammar path missing: $rel_path (resolved: $abs_path)" |
| 187 | + FAIL=$((FAIL + 1)) |
| 188 | + fi |
| 189 | + done <<< "$GRAMMAR_PATHS" |
| 190 | +else |
| 191 | + skip_test "grammar path consistency" "python3 not available or no grammars" |
| 192 | +fi |
| 193 | + |
| 194 | +# Snippet paths in package.json should match actual files |
| 195 | +SNIPPET_PATHS=$(python3 -c " |
| 196 | +import json |
| 197 | +p = json.load(open('$PROJECT_DIR/package.json')) |
| 198 | +for s in p.get('contributes', {}).get('snippets', []): |
| 199 | + print(s['path']) |
| 200 | +" 2>/dev/null || echo "") |
| 201 | + |
| 202 | +if [ -n "$SNIPPET_PATHS" ]; then |
| 203 | + while IFS= read -r rel_path; do |
| 204 | + abs_path="$PROJECT_DIR/${rel_path#./}" |
| 205 | + if [ -f "$abs_path" ]; then |
| 206 | + green " PASS: snippet path exists: $rel_path" |
| 207 | + PASS=$((PASS + 1)) |
| 208 | + else |
| 209 | + red " FAIL: snippet path missing: $rel_path" |
| 210 | + FAIL=$((FAIL + 1)) |
| 211 | + fi |
| 212 | + done <<< "$SNIPPET_PATHS" |
| 213 | +else |
| 214 | + skip_test "snippet path consistency" "python3 not available or no snippets" |
| 215 | +fi |
| 216 | + |
| 217 | +echo "" |
| 218 | + |
| 219 | +# ═══════════════════════════════════════════════════════════════════════ |
| 220 | +# Summary |
| 221 | +# ═══════════════════════════════════════════════════════════════════════ |
| 222 | +echo "═══════════════════════════════════════════════════════════════" |
| 223 | +printf " Results: " |
| 224 | +green "PASS=$PASS" | tr -d '\n' |
| 225 | +echo -n " " |
| 226 | +if [ "$FAIL" -gt 0 ]; then red "FAIL=$FAIL" | tr -d '\n'; else echo -n "FAIL=0"; fi |
| 227 | +echo -n " " |
| 228 | +if [ "$SKIP" -gt 0 ]; then yellow "SKIP=$SKIP"; else echo "SKIP=0"; fi |
| 229 | +echo "═══════════════════════════════════════════════════════════════" |
| 230 | + |
| 231 | +exit "$FAIL" |
0 commit comments