Skip to content

Commit 1d8fc5f

Browse files
hyperpolymathclaude
andcommitted
test: add E2E tests and CI workflow for vscode-k9
Tests cover extension manifest validation, TextMate grammar JSON validity, snippets structure, language configuration, and grammar/snippet path consistency. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 639f0c1 commit 1d8fc5f

2 files changed

Lines changed: 248 additions & 0 deletions

File tree

.github/workflows/e2e.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
name: E2E Tests
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: ['**']
8+
permissions:
9+
contents: read
10+
jobs:
11+
e2e:
12+
name: E2E tests
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
16+
- name: Run E2E tests
17+
run: bash tests/e2e.sh

tests/e2e.sh

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
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

Comments
 (0)