lsp: declarative action registry + cross-channel language server
Make `testium lsp` (and the testium_assist editor extension that spawns it)
work from every distribution channel: source, wheel, PyInstaller, Flatpak,
AppImage.
Two enablers:
1. Declarative ACTIONS registry. The TestItemActions parents (console, plot,
json_rpc) now declare their nested actions as a class attribute
`ACTIONS = {yaml_key: class}`, mirroring PARAMS. The base __init__ seeds
action_classes from type(self).ACTIONS; register_actions() is kept only as
an imperative escape hatch. lsp/schema.py reads ACTIONS directly, dropping
the inspect.getsource/AST walk that returned no actions in a frozen
PyInstaller build (no .py source on disk).
2. pygls bundled per channel. Kept as the pyproject [lsp] extra (lean
`pip install testium`), layered into each full-app channel:
- build_env.sh installs pygls into test/tmp/.venv (source run + PyInstaller
build env)
- AppImage installs the wheel as `…whl[lsp]`
- Flatpak adds a python3-lsp network-pip module (matches the manifest's
global --share=network)
- PyInstaller .spec collect_submodules(pygls/lsprotocol) + hiddenimports for
the lazily-imported lsp/lsp.server/lsp.schema
test/validation/lsp_smoke.py (run by run.sh before the suite) enforces both
per channel: `<channel> schema` must keep console/plot/json_rpc actions and
`<channel> lsp` must answer an initialize request without reporting pygls
missing. Verified for source mode; the other channels need a rebuild to verify.
DESIGN.md updated (declarative section + new "Language server across channels"
subsection + Recent fixes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
102
test/validation/lsp_smoke.py
Normal file
102
test/validation/lsp_smoke.py
Normal file
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Per-channel smoke test for the testium language server.
|
||||
|
||||
Given the channel's testium invocation as argv (e.g. ``flatpak run
|
||||
--command=testium org.testium.Testium``, a PyInstaller binary path, or
|
||||
``python -m testium``), verify two things end-to-end against that exact build:
|
||||
|
||||
1. ``<cmd> schema`` produces valid JSON whose item registry still includes the
|
||||
nested action sets (console/plot/json_rpc). This catches a frozen build
|
||||
that lost the actions — the failure mode the declarative ``ACTIONS``
|
||||
refactor fixed (no more ``inspect.getsource`` at runtime).
|
||||
2. ``<cmd> lsp`` starts a real language server: it must answer an LSP
|
||||
``initialize`` request with a capabilities result and must NOT report the
|
||||
pygls dependency as missing. This catches a channel that forgot to bundle
|
||||
the ``[lsp]`` extra.
|
||||
|
||||
Exits non-zero (with a diagnostic) on the first failure so the validation run
|
||||
fails loudly. Used by ``run.sh`` before launching the main suite.
|
||||
"""
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
EXPECTED_ACTION_PARENTS = ("console", "plot", "json_rpc")
|
||||
|
||||
|
||||
def fail(msg):
|
||||
print(f"LSP SMOKE: FAIL — {msg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def _extract_json(raw):
|
||||
"""Parse JSON from ``raw`` bytes, tolerating leading non-JSON noise.
|
||||
|
||||
The source-mode launcher (run.sh) may print env-setup lines before the
|
||||
schema JSON, so we fall back to parsing from the first ``{``.
|
||||
"""
|
||||
try:
|
||||
return json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
start = raw.find(b"{")
|
||||
if start < 0:
|
||||
raise
|
||||
return json.loads(raw[start:])
|
||||
|
||||
|
||||
def check_schema(cmd):
|
||||
try:
|
||||
out = subprocess.run(cmd + ["schema"], capture_output=True, timeout=120)
|
||||
except Exception as e: # noqa: BLE001
|
||||
fail(f"`{' '.join(cmd)} schema` could not run: {e}")
|
||||
if out.returncode != 0:
|
||||
fail(f"`schema` exited {out.returncode}: {out.stderr.decode()[:300]}")
|
||||
try:
|
||||
data = _extract_json(out.stdout)
|
||||
except json.JSONDecodeError as e:
|
||||
fail(f"`schema` output is not valid JSON: {e}")
|
||||
items = data.get("items", {})
|
||||
for parent in EXPECTED_ACTION_PARENTS:
|
||||
actions = (items.get(parent) or {}).get("actions") or {}
|
||||
if not actions:
|
||||
fail(f"schema item '{parent}' has no actions — a frozen build lost "
|
||||
f"the declarative ACTIONS (item keys: {sorted(items)[:8]}…)")
|
||||
print(f"LSP SMOKE: schema OK ({len(items)} items; actions present for "
|
||||
f"{', '.join(EXPECTED_ACTION_PARENTS)})")
|
||||
|
||||
|
||||
def check_lsp(cmd):
|
||||
body = json.dumps({
|
||||
"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
||||
"params": {"processId": None, "rootUri": None, "capabilities": {}},
|
||||
}).encode()
|
||||
msg = b"Content-Length: %d\r\n\r\n%s" % (len(body), body)
|
||||
try:
|
||||
out = subprocess.run(cmd + ["lsp"], input=msg,
|
||||
capture_output=True, timeout=30)
|
||||
stdout, stderr = out.stdout, out.stderr
|
||||
except subprocess.TimeoutExpired as e:
|
||||
# A server that stays alive past initialize is fine — it just never saw
|
||||
# a shutdown. Use whatever it wrote so far as the response.
|
||||
stdout, stderr = e.stdout or b"", e.stderr or b""
|
||||
blob = stdout + stderr
|
||||
if b"dependencies missing" in blob:
|
||||
fail("`lsp` reports the pygls dependency missing — this channel did "
|
||||
"not bundle the [lsp] extra.")
|
||||
if b'"capabilities"' not in stdout:
|
||||
fail("`lsp` did not return an initialize result. "
|
||||
f"stdout[:200]={stdout[:200]!r} stderr[:200]={stderr[:200]!r}")
|
||||
print("LSP SMOKE: lsp initialize OK (server answered with capabilities)")
|
||||
|
||||
|
||||
def main():
|
||||
cmd = sys.argv[1:]
|
||||
if not cmd:
|
||||
fail("usage: lsp_smoke.py <testium-invocation...>")
|
||||
check_schema(cmd)
|
||||
check_lsp(cmd)
|
||||
print("LSP SMOKE: PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -137,6 +137,13 @@ esac
|
||||
echo "-- validation mode: $MODE"
|
||||
echo "-- launch: ${CMD[*]}"
|
||||
|
||||
# ---------- LSP smoke check (this exact channel) ------------------------------
|
||||
# Verify `testium lsp` / `testium schema` work in the build under test before
|
||||
# running the suite: schema must keep its nested actions (declarative ACTIONS,
|
||||
# survives frozen builds) and the language server must start (pygls bundled).
|
||||
echo "-- LSP smoke check ($MODE)"
|
||||
"$VENV_PYTHON" "$SCRIPT_DIR/lsp_smoke.py" "${CMD[@]}"
|
||||
|
||||
exec "${CMD[@]}" -b \
|
||||
-d "python_bin=$VENV_PYTHON" \
|
||||
-d "validation_report_file=validation-$MODE" \
|
||||
|
||||
Reference in New Issue
Block a user